diff --git a/data_for_test.go b/data_for_test.go index ba41e2c..140f92f 100644 --- a/data_for_test.go +++ b/data_for_test.go @@ -568,6 +568,32 @@ var suites = []FixtureSuite{ }, }, }, + // #360: exec child exit code should propagate even when OTLP export fails + { + { + Name: "#360 exec child exit code preserved when export fails", + Config: FixtureConfig{ + CliArgs: []string{"exec", + "--endpoint", "{{endpoint}}", + "--timeout", "100ms", + "--", "/bin/sh", "-c", "exit 42", + }, + StopServerBeforeExec: true, + TestTimeoutMs: 2000, + IsLongTest: true, + }, + Expect: Results{ + Config: otelcli.DefaultConfig(), + }, + CheckFuncs: []CheckFunc{ + func(t *testing.T, f Fixture, r Results) { + if r.ExitCode != 42 { + t.Errorf("expected exit code 42 from child process, got %d", r.ExitCode) + } + }, + }, + }, + }, // otel-cli span with no OTLP config should do and print nothing { { diff --git a/otelcli/config.go b/otelcli/config.go index edc073f..1db35cf 100644 --- a/otelcli/config.go +++ b/otelcli/config.go @@ -373,11 +373,16 @@ func (c Config) SoftLogIfErr(err error) { } // SoftFail calls through to softLog (which logs only if otel-cli was run with the --verbose -// flag), then immediately exits - with status -1 by default, or 1 if --fail was -// set (a la `curl --fail`) +// flag), then immediately exits. When a child process exit code has been captured (e.g. from +// exec), that code is preserved. Otherwise exits 0 by default, or 1 with --fail. func (c Config) SoftFail(format string, a ...interface{}) { c.SoftLog(format, a...) + // preserve exec child exit code when available (#360) + if Diag.ExecExitCode != 0 { + os.Exit(Diag.ExecExitCode) + } + if c.Fail { os.Exit(1) } else { diff --git a/otelcli/exec.go b/otelcli/exec.go index 0a9df63..76632c9 100644 --- a/otelcli/exec.go +++ b/otelcli/exec.go @@ -152,6 +152,13 @@ func doExec(cmd *cobra.Command, args []string) { span.Attributes = append(span.Attributes, pidAttrs...) } + // capture the child's exit code before OTLP export so SoftFail can use it (#360) + if child.ProcessState != nil { + Diag.ExecExitCode = child.ProcessState.ExitCode() + } else { + Diag.ExecExitCode = 127 + } + cancelCtxDeadline() close(signals) <-signalsDone @@ -171,16 +178,6 @@ func doExec(cmd *cobra.Command, args []string) { config.SoftFail("client.Stop() failed: %s", err) } - // set the global exit code so main() can grab it and os.Exit() properly - // ProcessState is nil if the command failed to start - if child.ProcessState != nil { - Diag.ExecExitCode = child.ProcessState.ExitCode() - } else { - // command failed to start (e.g., command not found) - // use exit code 127 to match shell behavior for "command not found" - Diag.ExecExitCode = 127 - } - config.PropagateTraceparent(span, os.Stdout) }