Skip to content

Commit 5ccedac

Browse files
committed
fix(exec): preserve child exit code when OTLP export fails (#360)
SoftFail called os.Exit(0) or os.Exit(1) regardless of the child process exit code. When exec ran a command that exited 42, then OTLP export failed, the exit code was lost. Two changes: 1. Capture Diag.ExecExitCode before OTLP export (was after) 2. SoftFail checks Diag.ExecExitCode and uses it when non-zero Fixes #360 🤖 Claude <claude@anthropic.com>
1 parent ed5ef5d commit 5ccedac

3 files changed

Lines changed: 40 additions & 12 deletions

File tree

data_for_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,32 @@ var suites = []FixtureSuite{
568568
},
569569
},
570570
},
571+
// #360: exec child exit code should propagate even when OTLP export fails
572+
{
573+
{
574+
Name: "#360 exec child exit code preserved when export fails",
575+
Config: FixtureConfig{
576+
CliArgs: []string{"exec",
577+
"--endpoint", "{{endpoint}}",
578+
"--timeout", "100ms",
579+
"--", "/bin/sh", "-c", "exit 42",
580+
},
581+
StopServerBeforeExec: true,
582+
TestTimeoutMs: 2000,
583+
IsLongTest: true,
584+
},
585+
Expect: Results{
586+
Config: otelcli.DefaultConfig(),
587+
},
588+
CheckFuncs: []CheckFunc{
589+
func(t *testing.T, f Fixture, r Results) {
590+
if r.ExitCode != 42 {
591+
t.Errorf("expected exit code 42 from child process, got %d", r.ExitCode)
592+
}
593+
},
594+
},
595+
},
596+
},
571597
// otel-cli span with no OTLP config should do and print nothing
572598
{
573599
{

otelcli/config.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -373,11 +373,16 @@ func (c Config) SoftLogIfErr(err error) {
373373
}
374374

375375
// SoftFail calls through to softLog (which logs only if otel-cli was run with the --verbose
376-
// flag), then immediately exits - with status -1 by default, or 1 if --fail was
377-
// set (a la `curl --fail`)
376+
// flag), then immediately exits. When a child process exit code has been captured (e.g. from
377+
// exec), that code is preserved. Otherwise exits 0 by default, or 1 with --fail.
378378
func (c Config) SoftFail(format string, a ...interface{}) {
379379
c.SoftLog(format, a...)
380380

381+
// preserve exec child exit code when available (#360)
382+
if Diag.ExecExitCode != 0 {
383+
os.Exit(Diag.ExecExitCode)
384+
}
385+
381386
if c.Fail {
382387
os.Exit(1)
383388
} else {

otelcli/exec.go

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,13 @@ func doExec(cmd *cobra.Command, args []string) {
152152
span.Attributes = append(span.Attributes, pidAttrs...)
153153
}
154154

155+
// capture the child's exit code before OTLP export so SoftFail can use it (#360)
156+
if child.ProcessState != nil {
157+
Diag.ExecExitCode = child.ProcessState.ExitCode()
158+
} else {
159+
Diag.ExecExitCode = 127
160+
}
161+
155162
cancelCtxDeadline()
156163
close(signals)
157164
<-signalsDone
@@ -171,16 +178,6 @@ func doExec(cmd *cobra.Command, args []string) {
171178
config.SoftFail("client.Stop() failed: %s", err)
172179
}
173180

174-
// set the global exit code so main() can grab it and os.Exit() properly
175-
// ProcessState is nil if the command failed to start
176-
if child.ProcessState != nil {
177-
Diag.ExecExitCode = child.ProcessState.ExitCode()
178-
} else {
179-
// command failed to start (e.g., command not found)
180-
// use exit code 127 to match shell behavior for "command not found"
181-
Diag.ExecExitCode = 127
182-
}
183-
184181
config.PropagateTraceparent(span, os.Stdout)
185182
}
186183

0 commit comments

Comments
 (0)