Gotcha: PersistentPostRunE
only runs on successful commands in Cobra
In my recent post Lessons learned adding OpenTelemetry to a (Cobra) command-line Go tool, I wrote about how you can wire in OpenTelemetry to a command-line tool built with Cobra.
In it, I noted that to do so, you can use the PersistentPreRunE
and PersistentPostRunE
.
However, this morning my colleague Mario made me aware that it turns out this doesn't end up working, as he saw a lack of traces for commands that errored, when using the RunE
function.
This was unexpected, but appears to be somewhat known behaviour.
RunE
We can see this in action with the following code:
package main
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{
Use: "Example",
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
fmt.Println("Inside PersistentPreRunE")
return nil
},
PersistentPostRunE: func(cmd *cobra.Command, args []string) error {
fmt.Println("Inside PersistentPostRunE")
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
fmt.Println("Inside RunE")
if len(args) > 0 {
return fmt.Errorf("uhoh, you gave too many arguments, punk")
}
return nil
},
}
func main() {
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}
go.mod
module example
go 1.23.2
require github.com/spf13/cobra v1.8.1
require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
)
When running the command with no arguments:
# when running and not returning an error from the `RunE`
% go run .
Inside PersistentPreRunE
Inside RunE
Inside PersistentPostRunE
# when running and returning an error from `RunE`
% go run . args
Inside PersistentPreRunE
Inside RunE
Error: uhoh, you gave too many arguments, punk
Usage:
...
Notice that when there's an error, we don't see the Inside PersistentPostRunE
.
It appears that the solution is to use the OnFinalize
:
package main
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{
Use: "example",
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
fmt.Println("Inside PersistentPreRunE")
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
fmt.Println("Inside RunE")
if len(args) > 0 {
return fmt.Errorf("uhoh, you gave too many arguments, punk")
}
return nil
},
}
func main() {
cobra.OnFinalize(func() {
fmt.Println("Inside OnFinalize")
})
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}
Run
This is also true when we use a Run
function:
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Inside RunE")
if len(args) > 0 {
cobra.CheckErr(fmt.Errorf("uhoh, you gave too many arguments, punk"))
}
},
In this case, we see the following error:
% go run . args
Inside PersistentPreRunE
Inside RunE
Error: uhoh, you gave too many arguments, punk
exit status 1
However, the OnFinalize
doesn't seem to apply here, as if we have this code:
package main
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{
Use: "example",
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
fmt.Println("Inside PersistentPreRunE")
return nil
},
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Inside RunE")
if len(args) > 0 {
cobra.CheckErr(fmt.Errorf("uhoh, you gave too many arguments, punk"))
}
},
}
func main() {
cobra.OnFinalize(func() {
fmt.Println("Inside OnFinalize")
})
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}
Then this doesn't call OnFinalize
:
$ go run . args
Inside PersistentPreRunE
Inside RunE
Error: uhoh, you gave too many arguments, punk