From e5a288caaa74a18d848c24cd37d65f8450c15077 Mon Sep 17 00:00:00 2001 From: Cory Bennett Date: Fri, 28 Mar 2025 15:53:19 -0700 Subject: [PATCH 1/4] feat(provisioner): propagate trace info If tracing is enabled, propagate the trace information to the terraform provisioner via environment variables. This sets the `TRACEPARENT` environment variable using the default W3C trace propagators. Users can choose to continue the trace by adding new spans in the provisioner by reading from the environment like: ctx := env.ContextWithRemoteSpanContext(context.Background(), os.Environ()) --- provisioner/terraform/otelenv.go | 74 +++++++++++++++++++++++++++ provisioner/terraform/otelenv_test.go | 63 +++++++++++++++++++++++ provisioner/terraform/provision.go | 2 + 3 files changed, 139 insertions(+) create mode 100644 provisioner/terraform/otelenv.go create mode 100644 provisioner/terraform/otelenv_test.go diff --git a/provisioner/terraform/otelenv.go b/provisioner/terraform/otelenv.go new file mode 100644 index 0000000000000..1402f169183cc --- /dev/null +++ b/provisioner/terraform/otelenv.go @@ -0,0 +1,74 @@ +package terraform + +import ( + "context" + "fmt" + "slices" + "strings" + "unicode" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/propagation" +) + +// TODO: replace this with the upstream OTEL env propagation when it is +// released. + +// envCarrier is a propagation.TextMapCarrier that is used to extract or +// inject tracing environment variables. This is used with a +// propagation.TextMapPropagator +type envCarrier struct { + Env []string +} + +var _ propagation.TextMapCarrier = (*envCarrier)(nil) + +func toKey(key string) string { + key = strings.ToUpper(key) + key = strings.ReplaceAll(key, "-", "_") + return strings.Map(func(r rune) rune { + if unicode.IsLetter(r) || unicode.IsNumber(r) || r == '_' { + return r + } + return -1 + }, key) +} + +func (c *envCarrier) Set(key, value string) { + if c == nil { + return + } + key = toKey(key) + for i, e := range c.Env { + if strings.HasPrefix(e, key+"=") { + // don't directly update the slice so we don't modify the slice + // passed in + newEnv := slices.Clone(c.Env) + newEnv = append(newEnv[:i], append([]string{fmt.Sprintf("%s=%s", key, value)}, newEnv[i+1:]...)...) + c.Env = newEnv + return + } + } + c.Env = append(c.Env, fmt.Sprintf("%s=%s", key, value)) +} + +func (*envCarrier) Get(_ string) string { + // Get not necessary to inject environment variables + panic("Not implemented") +} + +func (*envCarrier) Keys() []string { + // Keys not necessary to inject environment variables + panic("Not implemented") +} + +// otelEnvInject will add add any necessary environment variables for the span +// found in the Context. If environment variables are already present +// in `environ` then they will be updated. If no variables are found the +// new ones will be appended. The new environment will be returned, `environ` +// will never be modified. +func otelEnvInject(ctx context.Context, environ []string) []string { + c := &envCarrier{Env: environ} + otel.GetTextMapPropagator().Inject(ctx, c) + return c.Env +} diff --git a/provisioner/terraform/otelenv_test.go b/provisioner/terraform/otelenv_test.go new file mode 100644 index 0000000000000..ef900c0b2d26c --- /dev/null +++ b/provisioner/terraform/otelenv_test.go @@ -0,0 +1,63 @@ +package terraform // nolint:testpackage + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/propagation" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/trace" +) + +type testIDGenerator struct{} + +var _ sdktrace.IDGenerator = (*testIDGenerator)(nil) + +func (testIDGenerator) NewIDs(ctx context.Context) (trace.TraceID, trace.SpanID) { + traceID, _ := trace.TraceIDFromHex("60d19e9e9abf2197c1d6d8f93e28ee2a") + spanID, _ := trace.SpanIDFromHex("a028bd951229a46f") + return traceID, spanID +} + +func (testIDGenerator) NewSpanID(ctx context.Context, traceID trace.TraceID) trace.SpanID { + spanID, _ := trace.SpanIDFromHex("a028bd951229a46f") + return spanID +} + +func TestOtelEnvInject(t *testing.T) { + t.Parallel() + testTraceProvider := sdktrace.NewTracerProvider( + sdktrace.WithSampler(sdktrace.AlwaysSample()), + sdktrace.WithIDGenerator(testIDGenerator{}), + ) + + tracer := testTraceProvider.Tracer("example") + ctx, span := tracer.Start(context.Background(), "testing") + defer span.End() + + input := []string{"PATH=/usr/bin:/bin"} + + otel.SetTextMapPropagator(propagation.TraceContext{}) + got := otelEnvInject(ctx, input) + require.Equal(t, []string{ + "PATH=/usr/bin:/bin", + "TRACEPARENT=00-60d19e9e9abf2197c1d6d8f93e28ee2a-a028bd951229a46f-01", + }, got) + + // verify we update rather than append + input = []string{ + "PATH=/usr/bin:/bin", + "TRACEPARENT=origTraceParent", + "TERM=xterm", + } + + otel.SetTextMapPropagator(propagation.TraceContext{}) + got = otelEnvInject(ctx, input) + require.Equal(t, []string{ + "PATH=/usr/bin:/bin", + "TRACEPARENT=00-60d19e9e9abf2197c1d6d8f93e28ee2a-a028bd951229a46f-01", + "TERM=xterm", + }, got) +} diff --git a/provisioner/terraform/provision.go b/provisioner/terraform/provision.go index 78068fc43c819..171deb35c4bbc 100644 --- a/provisioner/terraform/provision.go +++ b/provisioner/terraform/provision.go @@ -156,6 +156,7 @@ func (s *server) Plan( if err != nil { return provisionersdk.PlanErrorf("setup env: %s", err) } + env = otelEnvInject(ctx, env) vars, err := planVars(request) if err != nil { @@ -208,6 +209,7 @@ func (s *server) Apply( if err != nil { return provisionersdk.ApplyErrorf("provision env: %s", err) } + env = otelEnvInject(ctx, env) resp, err := e.apply( ctx, killCtx, env, sess, ) From fbab87757fd043cf94b4dc472093ec031a05f52b Mon Sep 17 00:00:00 2001 From: coryb Date: Mon, 7 Apr 2025 07:49:24 -0700 Subject: [PATCH 2/4] Update provisioner/terraform/otelenv_test.go Co-authored-by: Spike Curtis --- provisioner/terraform/otelenv_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/provisioner/terraform/otelenv_test.go b/provisioner/terraform/otelenv_test.go index ef900c0b2d26c..f5f9249263cee 100644 --- a/provisioner/terraform/otelenv_test.go +++ b/provisioner/terraform/otelenv_test.go @@ -21,7 +21,7 @@ func (testIDGenerator) NewIDs(ctx context.Context) (trace.TraceID, trace.SpanID) return traceID, spanID } -func (testIDGenerator) NewSpanID(ctx context.Context, traceID trace.TraceID) trace.SpanID { +func (testIDGenerator) NewSpanID(_ context.Context, _ trace.TraceID) trace.SpanID { spanID, _ := trace.SpanIDFromHex("a028bd951229a46f") return spanID } From 0a48cc5d37487cb9c1df6309a5445ad396f715c8 Mon Sep 17 00:00:00 2001 From: coryb Date: Mon, 7 Apr 2025 07:49:35 -0700 Subject: [PATCH 3/4] Update provisioner/terraform/otelenv_test.go Co-authored-by: Spike Curtis --- provisioner/terraform/otelenv_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/provisioner/terraform/otelenv_test.go b/provisioner/terraform/otelenv_test.go index f5f9249263cee..521856f1c3a91 100644 --- a/provisioner/terraform/otelenv_test.go +++ b/provisioner/terraform/otelenv_test.go @@ -15,7 +15,7 @@ type testIDGenerator struct{} var _ sdktrace.IDGenerator = (*testIDGenerator)(nil) -func (testIDGenerator) NewIDs(ctx context.Context) (trace.TraceID, trace.SpanID) { +func (testIDGenerator) NewIDs(_ context.Context) (trace.TraceID, trace.SpanID) { traceID, _ := trace.TraceIDFromHex("60d19e9e9abf2197c1d6d8f93e28ee2a") spanID, _ := trace.SpanIDFromHex("a028bd951229a46f") return traceID, spanID From 228410861961be1768ab3dcf78c4e0f9ebde2f04 Mon Sep 17 00:00:00 2001 From: Cory Bennett Date: Mon, 7 Apr 2025 16:51:08 +0000 Subject: [PATCH 4/4] update for feedback, remove unimplemented panics Simplified the slice copy/update logic. Removed the panics for non required interface functions, the impl was trivial, added simple tests to ensure they work as expected. --- provisioner/terraform/otelenv.go | 32 +++++++++++++------ ...elenv_test.go => otelenv_internal_test.go} | 24 +++++++++++++- 2 files changed, 46 insertions(+), 10 deletions(-) rename provisioner/terraform/{otelenv_test.go => otelenv_internal_test.go} (77%) diff --git a/provisioner/terraform/otelenv.go b/provisioner/terraform/otelenv.go index 1402f169183cc..681df25490854 100644 --- a/provisioner/terraform/otelenv.go +++ b/provisioner/terraform/otelenv.go @@ -43,23 +43,37 @@ func (c *envCarrier) Set(key, value string) { if strings.HasPrefix(e, key+"=") { // don't directly update the slice so we don't modify the slice // passed in - newEnv := slices.Clone(c.Env) - newEnv = append(newEnv[:i], append([]string{fmt.Sprintf("%s=%s", key, value)}, newEnv[i+1:]...)...) - c.Env = newEnv + c.Env = slices.Clone(c.Env) + c.Env[i] = fmt.Sprintf("%s=%s", key, value) return } } c.Env = append(c.Env, fmt.Sprintf("%s=%s", key, value)) } -func (*envCarrier) Get(_ string) string { - // Get not necessary to inject environment variables - panic("Not implemented") +func (c *envCarrier) Get(key string) string { + if c == nil { + return "" + } + key = toKey(key) + for _, e := range c.Env { + if strings.HasPrefix(e, key+"=") { + return strings.TrimPrefix(e, key+"=") + } + } + return "" } -func (*envCarrier) Keys() []string { - // Keys not necessary to inject environment variables - panic("Not implemented") +func (c *envCarrier) Keys() []string { + if c == nil { + return nil + } + keys := make([]string, len(c.Env)) + for i, e := range c.Env { + k, _, _ := strings.Cut(e, "=") + keys[i] = k + } + return keys } // otelEnvInject will add add any necessary environment variables for the span diff --git a/provisioner/terraform/otelenv_test.go b/provisioner/terraform/otelenv_internal_test.go similarity index 77% rename from provisioner/terraform/otelenv_test.go rename to provisioner/terraform/otelenv_internal_test.go index 521856f1c3a91..57be6e4cd0cc6 100644 --- a/provisioner/terraform/otelenv_test.go +++ b/provisioner/terraform/otelenv_internal_test.go @@ -1,4 +1,4 @@ -package terraform // nolint:testpackage +package terraform import ( "context" @@ -61,3 +61,25 @@ func TestOtelEnvInject(t *testing.T) { "TERM=xterm", }, got) } + +func TestEnvCarrierSet(t *testing.T) { + t.Parallel() + c := &envCarrier{ + Env: []string{"PATH=/usr/bin:/bin", "TERM=xterm"}, + } + c.Set("PATH", "/usr/local/bin") + c.Set("NEWVAR", "newval") + require.Equal(t, []string{ + "PATH=/usr/local/bin", + "TERM=xterm", + "NEWVAR=newval", + }, c.Env) +} + +func TestEnvCarrierKeys(t *testing.T) { + t.Parallel() + c := &envCarrier{ + Env: []string{"PATH=/usr/bin:/bin", "TERM=xterm"}, + } + require.Equal(t, []string{"PATH", "TERM"}, c.Keys()) +}