diff --git a/CHANGELOG.md b/CHANGELOG.md index 14ca6d1..d8313b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## [0.4.3] - 2024-03-11 + +Add injection of `{{traceparent}}` to `otel-cli exec` as default behavior, along with +the `otel-cli exec --tp-disable-inject` to turn it off (old behavior). + +### Added + +- `otel-cli exec echo {{traceparent}}` is now supported to pass traceparent to child process +- `otel-cli exec --tp-disable-inject` will disable this new default behavior + ## [0.4.2] - 2023-12-01 The Docker container now builds off `alpine:latest` instead of `scratch`. This diff --git a/README.md b/README.md index dec6943..aa4dc9d 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,11 @@ otel-cli exec --kind producer "otel-cli exec --kind consumer sleep 1" # used by span and exec. use --tp-ignore-env to ignore it even when present export TRACEPARENT=00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01 +# you can pass the traceparent to a child via arguments as well +# {{traceparent}} in any of the command's arguments will be replaced with the traceparent string +otel-cli exec --name "curl api" -- \ + curl -H 'traceparent: {{traceparent}}' https://myapi.com/v1/coolstuff + # create a span with a custom start/end time using either RFC3339, # same with the nanosecond extension, or Unix epoch, with/without nanos otel-cli span --start 2021-03-24T07:28:05.12345Z --end 2021-03-24T07:30:08.0001Z diff --git a/data_for_test.go b/data_for_test.go index bad44c0..3e3713c 100644 --- a/data_for_test.go +++ b/data_for_test.go @@ -902,7 +902,7 @@ var suites = []FixtureSuite{ // otel-cli exec runs otel-cli exec { { - Name: "otel-cli span exec (nested)", + Name: "otel-cli exec (nested)", Config: FixtureConfig{ CliArgs: []string{ "exec", "--name", "outer", "--endpoint", "{{endpoint}}", "--fail", "--verbose", "--", @@ -915,6 +915,40 @@ var suites = []FixtureSuite{ }, }, }, + // otel-cli exec echo "{{traceparent}}" and otel-cli exec --tp-disable-inject + { + { + Name: "otel-cli exec with arg injection injects the traceparent", + Config: FixtureConfig{ + CliArgs: []string{ + "exec", "--endpoint", "{{endpoint}}", + "--force-trace-id", "e39280f2980af3a8600ae98c74f2dabf", "--force-span-id", "023eee2731392b4d", + "--", + "echo", "{{traceparent}}"}, + }, + Expect: Results{ + Config: otelcli.DefaultConfig().WithEndpoint("{{endpoint}}"), + CliOutput: "00-e39280f2980af3a8600ae98c74f2dabf-023eee2731392b4d-01\n", + SpanCount: 1, + }, + }, + { + Name: "otel-cli exec --tp-disable-inject returns the {{traceparent}} tag unmodified", + Config: FixtureConfig{ + CliArgs: []string{ + "exec", "--endpoint", "{{endpoint}}", + "--force-trace-id", "e39280f2980af3a8600ae98c74f2dabf", "--force-span-id", "023eee2731392b4d", + "--tp-disable-inject", + "--", + "echo", "{{traceparent}}"}, + }, + Expect: Results{ + Config: otelcli.DefaultConfig().WithEndpoint("{{endpoint}}"), + CliOutput: "{{traceparent}}\n", + SpanCount: 1, + }, + }, + }, // validate OTEL_EXPORTER_OTLP_PROTOCOL / --protocol { // --protocol diff --git a/otelcli/config.go b/otelcli/config.go index cc9a76d..28f08e6 100644 --- a/otelcli/config.go +++ b/otelcli/config.go @@ -56,6 +56,7 @@ func DefaultConfig() Config { BackgroundWait: false, BackgroundSkipParentPidCheck: false, ExecCommandTimeout: "", + ExecTpDisableInject: false, StatusCanaryCount: 1, StatusCanaryInterval: "", SpanStartTime: "now", @@ -109,7 +110,8 @@ type Config struct { BackgroundWait bool `json:"background_wait" env:""` BackgroundSkipParentPidCheck bool `json:"background_skip_parent_pid_check"` - ExecCommandTimeout string `json:"exec_command_timeout" env:"OTEL_CLI_EXEC_CMD_TIMEOUT"` + ExecCommandTimeout string `json:"exec_command_timeout" env:"OTEL_CLI_EXEC_CMD_TIMEOUT"` + ExecTpDisableInject bool `json:"exec_tp_disable_inject" env:"OTEL_CLI_EXEC_TP_DISALBE_INJECT"` StatusCanaryCount int `json:"status_canary_count"` StatusCanaryInterval string `json:"status_canary_interval"` @@ -232,6 +234,7 @@ func (c Config) ToStringMap() map[string]string { "background_wait": strconv.FormatBool(c.BackgroundWait), "background_skip_pid_check": strconv.FormatBool(c.BackgroundSkipParentPidCheck), "exec_command_timeout": c.ExecCommandTimeout, + "exec_tp_disable_inject": strconv.FormatBool(c.ExecTpDisableInject), "span_start_time": c.SpanStartTime, "span_end_time": c.SpanEndTime, "event_name": c.EventName, diff --git a/otelcli/exec.go b/otelcli/exec.go index 6602fa3..f8c02c8 100644 --- a/otelcli/exec.go +++ b/otelcli/exec.go @@ -12,6 +12,7 @@ import ( "time" "github.com/equinix-labs/otel-cli/otlpclient" + "github.com/equinix-labs/otel-cli/w3c/traceparent" "github.com/spf13/cobra" tracev1 "go.opentelemetry.io/proto/otlp/trace/v1" ) @@ -47,12 +48,20 @@ otel-cli exec -s "outer span" 'otel-cli exec -s "inner span" sleep 1'`, "timeout for the child process, when 0 otel-cli will wait forever", ) + cmd.Flags().BoolVar( + &config.ExecTpDisableInject, + "tp-disable-inject", + defaults.ExecTpDisableInject, + "disable automatically replacing {{traceparent}} with a traceparent", + ) + return &cmd } func doExec(cmd *cobra.Command, args []string) { ctx := cmd.Context() config := getConfig(ctx) + span := config.NewProtobufSpan() // put the command in the attributes, before creating the span so it gets picked up config.Attributes["command"] = args[0] @@ -60,20 +69,49 @@ func doExec(cmd *cobra.Command, args []string) { // no deadline if there is no command timeout set cancelCtxDeadline := func() {} + // fork the context for the command so its deadline doesn't impact the otlpclient ctx cmdCtx := ctx cmdTimeout := config.ParseExecCommandTimeout() if cmdTimeout > 0 { cmdCtx, cancelCtxDeadline = context.WithDeadline(ctx, time.Now().Add(cmdTimeout)) } + // pass the existing env but add the latest TRACEPARENT carrier so e.g. + // otel-cli exec 'otel-cli exec sleep 1' will relate the spans automatically + childEnv := []string{} + + // set the traceparent to the current span to be available to the child process + var tp traceparent.Traceparent + if config.GetIsRecording() { + tp = otlpclient.TraceparentFromProtobufSpan(span, config.GetIsRecording()) + childEnv = append(childEnv, fmt.Sprintf("TRACEPARENT=%s", tp.Encode())) + // when not recording, and a traceparent is available, pass it through + } else if !config.TraceparentIgnoreEnv { + tp := config.LoadTraceparent() + if tp.Initialized { + childEnv = append(childEnv, fmt.Sprintf("TRACEPARENT=%s", tp.Encode())) + } + } + var child *exec.Cmd if len(args) > 1 { - // CSV-join the arguments to send as an attribute buf := bytes.NewBuffer([]byte{}) - csv.NewWriter(buf).WriteAll([][]string{args[1:]}) + tpArgs := make([]string, len(args)-1) + + if config.ExecTpDisableInject { + copy(tpArgs, args[1:]) + } else { + // loop over the args replacing {{traceparent}} with the current tp + for i, arg := range args[1:] { + tpArgs[i] = strings.Replace(arg, "{{traceparent}}", tp.Encode(), -1) + } + } + + // CSV-join the arguments to send as an attribute + csv.NewWriter(buf).WriteAll([][]string{tpArgs}) config.Attributes["arguments"] = buf.String() - child = exec.CommandContext(cmdCtx, args[0], args[1:]...) + child = exec.CommandContext(cmdCtx, args[0], tpArgs...) } else { child = exec.CommandContext(cmdCtx, args[0]) } @@ -83,30 +121,13 @@ func doExec(cmd *cobra.Command, args []string) { child.Stdout = os.Stdout child.Stderr = os.Stderr - // pass the existing env but add the latest TRACEPARENT carrier so e.g. - // otel-cli exec 'otel-cli exec sleep 1' will relate the spans automatically - child.Env = []string{} - // grab everything BUT the TRACEPARENT envvar for _, env := range os.Environ() { if !strings.HasPrefix(env, "TRACEPARENT=") { - child.Env = append(child.Env, env) - } - } - - span := config.NewProtobufSpan() - - // set the traceparent to the current span to be available to the child process - if config.GetIsRecording() { - tp := otlpclient.TraceparentFromProtobufSpan(span, config.GetIsRecording()) - child.Env = append(child.Env, fmt.Sprintf("TRACEPARENT=%s", tp.Encode())) - // when not recording, and a traceparent is available, pass it through - } else if !config.TraceparentIgnoreEnv { - tp := config.LoadTraceparent() - if tp.Initialized { - child.Env = append(child.Env, fmt.Sprintf("TRACEPARENT=%s", tp.Encode())) + childEnv = append(childEnv, env) } } + child.Env = childEnv // ctrl-c (sigint) is forwarded to the child process signals := make(chan os.Signal, 10) @@ -119,6 +140,7 @@ func doExec(cmd *cobra.Command, args []string) { close(signalsDone) }() + span.StartTimeUnixNano = uint64(time.Now().UnixNano()) if err := child.Run(); err != nil { span.Status = &tracev1.Status{ Message: fmt.Sprintf("exec command failed: %s", err),