From d1417fcb8954f7e7429ca866be02c77b0d4bd09f Mon Sep 17 00:00:00 2001 From: Amy Tobey Date: Wed, 22 Sep 2021 09:30:42 -0700 Subject: [PATCH 1/4] add some more helpers --- otelhelpers/env_traceparent.go | 63 +++++++++++++++++++++++++++++++--- 1 file changed, 58 insertions(+), 5 deletions(-) diff --git a/otelhelpers/env_traceparent.go b/otelhelpers/env_traceparent.go index a9035cd..22b18f3 100644 --- a/otelhelpers/env_traceparent.go +++ b/otelhelpers/env_traceparent.go @@ -1,7 +1,11 @@ +// otelhelpers is a package of helper functions for dealing with otel +// traceparent propagation over files and environment variables. package otelhelpers import ( + "bytes" "context" + "io/ioutil" "os" "go.opentelemetry.io/otel" @@ -14,11 +18,60 @@ import ( func ContextWithEnvTraceparent(ctx context.Context) context.Context { traceparent := os.Getenv("TRACEPARENT") if traceparent != "" { - carrier := SimpleCarrier{} - carrier.Set("traceparent", traceparent) - prop := otel.GetTextMapPropagator() - return prop.Extract(ctx, carrier) + return ContextWithTraceparentString(ctx, traceparent) } - return ctx } + +// ContextWithLinuxCmdlineTraceparent looks in /proc/cmdline for a traceparent= +// command line option and returns the context with that value as traceparent +// if it's there. Does no validation. Returns the original context if there is +// no cmdline option or if there's an error doing the read. +func ContextWithLinuxCmdlineTraceparent(ctx context.Context) context.Context { + tp, err := tpFromCmdline("/proc/cmdline") + if err != nil { + // what to do with error? is there a way to hit the otel error handler infra? + return ctx + } + + return ContextWithTraceparentString(ctx, tp) +} + +// ContextWithCmdlineOrEnvTraceparent checks the environment variable first, +// then /proc/cmdline and returns a context with them set, if available. When +// both are present, the cmdline is prioritized. When neither is present, +// the original context is returned as-is. +func ContextWithCmdlineOrEnvTraceparent(ctx context.Context) context.Context { + ctx = ContextWithEnvTraceparent(ctx) + return ContextWithLinuxCmdlineTraceparent(ctx) +} + +// ContextWithTraceparentString takes a W3C traceparent string, uses the otel +// carrier code to get it into a context it returns ready to go. +func ContextWithTraceparentString(ctx context.Context, traceparent string) context.Context { + carrier := SimpleCarrier{} + carrier.Set("traceparent", traceparent) + prop := otel.GetTextMapPropagator() + return prop.Extract(ctx, carrier) +} + +// tpFromCmdline reads a /proc/cmdline style file, parses it, and returns whatever +// value is present for "traceparent=". +func tpFromCmdline(file string) (string, error) { + data, err := ioutil.ReadFile(file) + if err != nil { + return "", err + } + + if bytes.Contains(data, []byte("traceparent=")) { + kvpairs := bytes.Split(data, []byte(" ")) + for _, kv := range kvpairs { + parts := bytes.SplitN(kv, []byte("="), 2) + if string(parts[0]) == "traceparent" { + return string(parts[1]), nil + } + } + } + + return "", nil +} From 16561ffc208cc95a6b0a42986990e38fd49806a6 Mon Sep 17 00:00:00 2001 From: Amy Tobey Date: Wed, 22 Sep 2021 09:31:18 -0700 Subject: [PATCH 2/4] rename file to align with its contents --- otelhelpers/{env_traceparent.go => context_traceparent.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename otelhelpers/{env_traceparent.go => context_traceparent.go} (100%) diff --git a/otelhelpers/env_traceparent.go b/otelhelpers/context_traceparent.go similarity index 100% rename from otelhelpers/env_traceparent.go rename to otelhelpers/context_traceparent.go From 64bae83d00a2cdbc07b3768b1d12fa5e93664c29 Mon Sep 17 00:00:00 2001 From: Amy Tobey Date: Wed, 22 Sep 2021 11:06:37 -0700 Subject: [PATCH 3/4] add more helpers and some tests for them Not perfect test coverage but enough to get *some* confidence it's mostly right. More to come. Signed-off-by: Amy Tobey --- go.mod | 1 + otelhelpers/context_traceparent.go | 14 ++++- otelhelpers/context_traceparent_test.go | 76 +++++++++++++++++++++++++ otelhelpers/simple_carrier_test.go | 60 +++++++++++++++++++ 4 files changed, 149 insertions(+), 2 deletions(-) create mode 100644 otelhelpers/context_traceparent_test.go create mode 100644 otelhelpers/simple_carrier_test.go diff --git a/go.mod b/go.mod index 349e9a9..93338c0 100644 --- a/go.mod +++ b/go.mod @@ -7,5 +7,6 @@ require ( go.opentelemetry.io/otel v1.0.0-RC2 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.0.0-RC2 go.opentelemetry.io/otel/sdk v1.0.0-RC2 + go.opentelemetry.io/otel/trace v1.0.0-RC2 google.golang.org/grpc v1.39.0 ) diff --git a/otelhelpers/context_traceparent.go b/otelhelpers/context_traceparent.go index 22b18f3..3e6c6b4 100644 --- a/otelhelpers/context_traceparent.go +++ b/otelhelpers/context_traceparent.go @@ -27,7 +27,8 @@ func ContextWithEnvTraceparent(ctx context.Context) context.Context { // command line option and returns the context with that value as traceparent // if it's there. Does no validation. Returns the original context if there is // no cmdline option or if there's an error doing the read. -func ContextWithLinuxCmdlineTraceparent(ctx context.Context) context.Context { +// This is Linux-only but should be safe on other operating systems. +func ContextWithCmdlineTraceparent(ctx context.Context) context.Context { tp, err := tpFromCmdline("/proc/cmdline") if err != nil { // what to do with error? is there a way to hit the otel error handler infra? @@ -43,7 +44,7 @@ func ContextWithLinuxCmdlineTraceparent(ctx context.Context) context.Context { // the original context is returned as-is. func ContextWithCmdlineOrEnvTraceparent(ctx context.Context) context.Context { ctx = ContextWithEnvTraceparent(ctx) - return ContextWithLinuxCmdlineTraceparent(ctx) + return ContextWithCmdlineTraceparent(ctx) } // ContextWithTraceparentString takes a W3C traceparent string, uses the otel @@ -55,6 +56,15 @@ func ContextWithTraceparentString(ctx context.Context, traceparent string) conte return prop.Extract(ctx, carrier) } +// TraceparentStringFromContext gets the current trace from the context and +// returns a W3C traceparent string. +func TraceparentStringFromContext(ctx context.Context) string { + carrier := SimpleCarrier{} + prop := otel.GetTextMapPropagator() + prop.Inject(ctx, carrier) + return carrier.Get("traceparent") +} + // tpFromCmdline reads a /proc/cmdline style file, parses it, and returns whatever // value is present for "traceparent=". func tpFromCmdline(file string) (string, error) { diff --git a/otelhelpers/context_traceparent_test.go b/otelhelpers/context_traceparent_test.go new file mode 100644 index 0000000..edc3221 --- /dev/null +++ b/otelhelpers/context_traceparent_test.go @@ -0,0 +1,76 @@ +package otelhelpers + +import ( + "context" + "io/ioutil" + "os" + "testing" + + "github.com/equinix-labs/otel-init-go/otelinit" + "go.opentelemetry.io/otel/trace" +) + +func TestMain(m *testing.M) { + // Many of the tests here won't work at all if otel is in non-recording mode + // so we set a default endpoint and let it fail in the background. If there + // happens to be a listener it will connnect but should not receive spans. + os.Setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "localhost:4317") + otelinit.InitOpenTelemetry(context.Background(), "otel-init-go-helpers-test") + + os.Exit(m.Run()) +} + +func TestContextWithEnvTraceparent(t *testing.T) { + // make sure the environment variable isn't polluting test state + os.Unsetenv("TRACEPARENT") + + // trace id should not change, because there's no envvar and no file + ctx := ContextWithEnvTraceparent(context.Background()) + sc := trace.SpanContextFromContext(ctx) + if sc.HasTraceID() { + t.Error("traceparent detected where there should be none") + } + + os.Setenv("TRACEPARENT", "00-f61fc53f926e07a9c3893b1a722e1b65-7a2d6a804f3de137-01") + ctx = ContextWithEnvTraceparent(context.Background()) + sc = trace.SpanContextFromContext(ctx) + if sc.TraceID().String() != "f61fc53f926e07a9c3893b1a722e1b65" { + t.Errorf("no trace id where one is expected. got: %q", sc.TraceID().String()) + } + if sc.SpanID().String() != "7a2d6a804f3de137" { + t.Errorf("no span id where one is expected. got: %q", sc.SpanID().String()) + } + if !sc.IsSampled() { + t.Error("expected sampling to be enabled but it is not") + } +} + +func TestTpFromCmdline(t *testing.T) { + testTp := "00-f61fc53f926e07a9c3893b1a722e1b65-7a2d6a804f3de137-01" + testCmdlines := []string{ + "traceparent=00-f61fc53f926e07a9c3893b1a722e1b65-7a2d6a804f3de137-01", + "foo=bar initrd=lol root=wheeeeeeeeeeee-fun traceparent=00-f61fc53f926e07a9c3893b1a722e1b65-7a2d6a804f3de137-01", + "traceparent=00-f61fc53f926e07a9c3893b1a722e1b65-7a2d6a804f3de137-01 foo=bar initrd=lol root=wheeeeeeeeeeee-fun", + "kimi=ga baka=desu traceparent=00-f61fc53f926e07a9c3893b1a722e1b65-7a2d6a804f3de137-01 foo=bar initrd=lol root=wheeeeeeeeeeee-fun", + } + + for _, cmdline := range testCmdlines { + file, err := ioutil.TempFile(t.TempDir(), "go-test-otel-init-go") + if err != nil { + t.Fatalf("unable to create tempfile for testing: %s", err) + } + defer os.Remove(file.Name()) + + // write out a cmdline file for test + file.WriteString(cmdline) + file.Close() + + got, err := tpFromCmdline(file.Name()) + if err != nil { + t.Errorf("reading cmdline test file failed unexpectedly: %s", err) + } + if got != testTp { + t.Errorf("tpFromCmdline comparison failed, expected '%s', got '%s'", testTp, got) + } + } +} diff --git a/otelhelpers/simple_carrier_test.go b/otelhelpers/simple_carrier_test.go new file mode 100644 index 0000000..cff3015 --- /dev/null +++ b/otelhelpers/simple_carrier_test.go @@ -0,0 +1,60 @@ +package otelhelpers + +import ( + "context" + "testing" + + "go.opentelemetry.io/otel" +) + +func TestSimpleCarrier(t *testing.T) { + carrier := SimpleCarrier{} + carrier.Clear() // clean up after other tests + + // traceparent is the only key supported by SimpleCarrier + got := carrier.Get("traceparent") + if got != "" { + t.Errorf("got a non-empty traceparent value '%s' where empty string was expected", got) + } + + carrier.Set("foobar", "baz") + if carrier.Get("foobar") != "baz" { + t.Error("did not get the expected value back in Set/Get test") + } + + // traceparent is supported so this should work fine + tp := "00-b122b620341449410b9cd900c96d459d-aa21cda35388b694-01" + carrier.Set("traceparent", tp) + + // even though 2 keys have been set at this point, the carrier only returns + // one key, traceparent + keys := carrier.Keys() + if len(keys) != 2 { + t.Errorf("expected exactly 2 keys from Keys() but instead got %q", keys) + } + + // make sure the value round-trips in one piece + got = carrier.Get("traceparent") + if got != tp { + t.Errorf("expected traceparent value '%s' but got '%s'", tp, got) + } + + // it's impractical to test the internal state of otel-go, so the next best + // thing is to round-trip our traceparent through it and make sure it comes + // back as expected + prop := otel.GetTextMapPropagator() + ctx := prop.Extract(context.Background(), carrier) + if ctx == nil { + t.Errorf("expected a context but got nil, likely a problem in otel? this shouldn't happen...") + } + + // try to round trip the traceparent back out of that context ^^ + rtCarrier := SimpleCarrier{} + prop.Inject(ctx, rtCarrier) + got = carrier.Get("traceparent") + if got != tp { + t.Errorf("round-tripping traceparent through a context failed, expected '%s', got '%s'", tp, got) + } + + carrier.Clear() // clean up for other tests +} From 9e17bec7c8659b6e5f6376b5bd27cd2f7e5a1d19 Mon Sep 17 00:00:00 2001 From: Amy Tobey Date: Wed, 22 Sep 2021 12:50:29 -0700 Subject: [PATCH 4/4] fix comment that's what I get for copying from otel-cli Signed-off-by: Amy Tobey --- otelhelpers/simple_carrier_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/otelhelpers/simple_carrier_test.go b/otelhelpers/simple_carrier_test.go index cff3015..dc30e7e 100644 --- a/otelhelpers/simple_carrier_test.go +++ b/otelhelpers/simple_carrier_test.go @@ -26,8 +26,7 @@ func TestSimpleCarrier(t *testing.T) { tp := "00-b122b620341449410b9cd900c96d459d-aa21cda35388b694-01" carrier.Set("traceparent", tp) - // even though 2 keys have been set at this point, the carrier only returns - // one key, traceparent + // we've set 2 keys so far, and both should get returned keys := carrier.Keys() if len(keys) != 2 { t.Errorf("expected exactly 2 keys from Keys() but instead got %q", keys)