Skip to content

Commit 80b1d7c

Browse files
committed
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())
1 parent 9bc727e commit 80b1d7c

File tree

3 files changed

+139
-0
lines changed

3 files changed

+139
-0
lines changed

provisioner/terraform/otelenv.go

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package terraform
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"slices"
7+
"strings"
8+
"unicode"
9+
10+
"go.opentelemetry.io/otel"
11+
"go.opentelemetry.io/otel/propagation"
12+
)
13+
14+
// TODO: replace this with the upstream OTEL env propagation when it is
15+
// released.
16+
17+
// envCarrier is a propagation.TextMapCarrier that is used to extract or
18+
// inject tracing environment variables. This is used with a
19+
// propagation.TextMapPropagator
20+
type envCarrier struct {
21+
Env []string
22+
}
23+
24+
var _ propagation.TextMapCarrier = (*envCarrier)(nil)
25+
26+
func toKey(key string) string {
27+
key = strings.ToUpper(key)
28+
key = strings.ReplaceAll(key, "-", "_")
29+
return strings.Map(func(r rune) rune {
30+
if unicode.IsLetter(r) || unicode.IsNumber(r) || r == '_' {
31+
return r
32+
}
33+
return -1
34+
}, key)
35+
}
36+
37+
func (c *envCarrier) Set(key, value string) {
38+
if c == nil {
39+
return
40+
}
41+
key = toKey(key)
42+
for i, e := range c.Env {
43+
if strings.HasPrefix(e, key+"=") {
44+
// don't directly update the slice so we don't modify the slice
45+
// passed in
46+
newEnv := slices.Clone(c.Env)
47+
newEnv = append(newEnv[:i], append([]string{fmt.Sprintf("%s=%s", key, value)}, newEnv[i+1:]...)...)
48+
c.Env = newEnv
49+
return
50+
}
51+
}
52+
c.Env = append(c.Env, fmt.Sprintf("%s=%s", key, value))
53+
}
54+
55+
func (_ *envCarrier) Get(_ string) string {
56+
// Get not necessary to inject environment variables
57+
panic("Not implemented")
58+
}
59+
60+
func (_ *envCarrier) Keys() []string {
61+
// Keys not necessary to inject environment variables
62+
panic("Not implemented")
63+
}
64+
65+
// otelEnvInject will add add any necessary environment variables for the span
66+
// found in the Context. If environment variables are already present
67+
// in `environ` then they will be updated. If no variables are found the
68+
// new ones will be appended. The new environment will be returned, `environ`
69+
// will never be modified.
70+
func otelEnvInject(ctx context.Context, environ []string) []string {
71+
c := &envCarrier{Env: environ}
72+
otel.GetTextMapPropagator().Inject(ctx, c)
73+
return c.Env
74+
}

provisioner/terraform/otelenv_test.go

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package terraform // nolint:testpackage
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
"go.opentelemetry.io/otel"
9+
"go.opentelemetry.io/otel/propagation"
10+
sdktrace "go.opentelemetry.io/otel/sdk/trace"
11+
"go.opentelemetry.io/otel/trace"
12+
)
13+
14+
type testIDGenerator struct{}
15+
16+
var _ sdktrace.IDGenerator = (*testIDGenerator)(nil)
17+
18+
func (testIDGenerator) NewIDs(ctx context.Context) (trace.TraceID, trace.SpanID) {
19+
traceID, _ := trace.TraceIDFromHex("60d19e9e9abf2197c1d6d8f93e28ee2a")
20+
spanID, _ := trace.SpanIDFromHex("a028bd951229a46f")
21+
return traceID, spanID
22+
}
23+
24+
func (testIDGenerator) NewSpanID(ctx context.Context, traceID trace.TraceID) trace.SpanID {
25+
spanID, _ := trace.SpanIDFromHex("a028bd951229a46f")
26+
return spanID
27+
}
28+
29+
func TestOtelEnvInject(t *testing.T) {
30+
t.Parallel()
31+
testTraceProvider := sdktrace.NewTracerProvider(
32+
sdktrace.WithSampler(sdktrace.AlwaysSample()),
33+
sdktrace.WithIDGenerator(testIDGenerator{}),
34+
)
35+
36+
tracer := testTraceProvider.Tracer("example")
37+
ctx, span := tracer.Start(context.Background(), "testing")
38+
defer span.End()
39+
40+
input := []string{"PATH=/usr/bin:/bin"}
41+
42+
otel.SetTextMapPropagator(propagation.TraceContext{})
43+
got := otelEnvInject(ctx, input)
44+
require.Equal(t, []string{
45+
"PATH=/usr/bin:/bin",
46+
"TRACEPARENT=00-60d19e9e9abf2197c1d6d8f93e28ee2a-a028bd951229a46f-01",
47+
}, got)
48+
49+
// verify we update rather than append
50+
input = []string{
51+
"PATH=/usr/bin:/bin",
52+
"TRACEPARENT=origTraceParent",
53+
"TERM=xterm",
54+
}
55+
56+
otel.SetTextMapPropagator(propagation.TraceContext{})
57+
got = otelEnvInject(ctx, input)
58+
require.Equal(t, []string{
59+
"PATH=/usr/bin:/bin",
60+
"TRACEPARENT=00-60d19e9e9abf2197c1d6d8f93e28ee2a-a028bd951229a46f-01",
61+
"TERM=xterm",
62+
}, got)
63+
}

provisioner/terraform/provision.go

+2
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ func (s *server) Plan(
156156
if err != nil {
157157
return provisionersdk.PlanErrorf("setup env: %s", err)
158158
}
159+
env = otelEnvInject(ctx, env)
159160

160161
vars, err := planVars(request)
161162
if err != nil {
@@ -208,6 +209,7 @@ func (s *server) Apply(
208209
if err != nil {
209210
return provisionersdk.ApplyErrorf("provision env: %s", err)
210211
}
212+
env = otelEnvInject(ctx, env)
211213
resp, err := e.apply(
212214
ctx, killCtx, env, sess,
213215
)

0 commit comments

Comments
 (0)