Skip to content

Commit 4459db5

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 4459db5

File tree

3 files changed

+137
-0
lines changed

3 files changed

+137
-0
lines changed

provisioner/terraform/otelenv.go

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

provisioner/terraform/otelenv_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package terraform
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 (g *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 (g *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+
testTraceProvider := sdktrace.NewTracerProvider(
31+
sdktrace.WithSampler(sdktrace.AlwaysSample()),
32+
sdktrace.WithIDGenerator(&testIDGenerator{}),
33+
)
34+
35+
tracer := testTraceProvider.Tracer("example")
36+
ctx, span := tracer.Start(context.Background(), "testing")
37+
defer span.End()
38+
39+
input := []string{"PATH=/usr/bin:/bin"}
40+
41+
otel.SetTextMapPropagator(propagation.TraceContext{})
42+
got := otelEnvInject(ctx, input)
43+
require.Equal(t, []string{
44+
"PATH=/usr/bin:/bin",
45+
"TRACEPARENT=00-60d19e9e9abf2197c1d6d8f93e28ee2a-a028bd951229a46f-01",
46+
}, got)
47+
48+
// verify we update rather than append
49+
input = []string{
50+
"PATH=/usr/bin:/bin",
51+
"TRACEPARENT=origTraceParent",
52+
"TERM=xterm",
53+
}
54+
55+
otel.SetTextMapPropagator(propagation.TraceContext{})
56+
got = otelEnvInject(ctx, input)
57+
require.Equal(t, []string{
58+
"PATH=/usr/bin:/bin",
59+
"TRACEPARENT=00-60d19e9e9abf2197c1d6d8f93e28ee2a-a028bd951229a46f-01",
60+
"TERM=xterm",
61+
}, got)
62+
}

provisioner/terraform/provision.go

Lines changed: 2 additions & 0 deletions
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)