Using Command Middleware¶
GTB's middleware system lets you add cross-cutting behaviour (logging, timing, authentication checks, telemetry) to your CLI commands without duplicating code in every handler. Since v0.5 middleware is wired automatically when a parent attaches a child via setup.Command.Register โ there is no separate "wrap with middleware" call.
Registering global middleware¶
Global middleware applies to every command in your tool. This is typically done by the framework's root constructor:
import (
"gitlab.com/phpboyscout/go-tool-base/pkg/setup"
)
func init() {
setup.RegisterGlobalMiddleware(
setup.WithRecovery(logger),
setup.WithTiming(logger),
)
}
The framework calls setup.Seal() once before building the command tree, so further Register*Middleware calls after sealing panic โ register at process start (init() or before NewCmdRoot).
Registering feature middleware¶
Feature middleware only applies to commands whose Feature key matches. Register it in the feature package's init():
package chat
import (
"gitlab.com/phpboyscout/go-tool-base/pkg/props"
"gitlab.com/phpboyscout/go-tool-base/pkg/setup"
)
func init() {
// This middleware ONLY runs for commands wrapped with FeatureCmd("chat").
setup.RegisterMiddleware(props.FeatureCmd("chat"),
setup.WithAuthCheck("chat.api_key", "chat.model"),
)
}
A command picks up that middleware by carrying the matching feature key:
func NewCmdChat(p *props.Props) *setup.Command {
return setup.Wrap("chat", &cobra.Command{Use: "chat", RunE: runChat})
}
Built-in middleware¶
WithRecovery¶
Catches panics and converts them into errors. Without it, an unhandled panic terminates the process โ with it, you get a clean Error: panic: ... log line and a non-zero exit.
WithTiming¶
Logs the wall-clock duration of every command at INFO level.
WithAuthCheck¶
Validates that required configuration keys are non-empty before running the command โ short-circuiting with a useful error instead of failing partway through.
WithTelemetry¶
Emits structured command-invocation events through the telemetry collector. Active when the telemetry feature is enabled and a backend is configured.
Attaching commands¶
Use *setup.Command.Register(child...) from the parent. Middleware is applied at attach time:
func NewCmdMyTool(p *props.Props) *setup.Command {
rootCmd := root.NewCmdRoot(p) // *setup.Command
rootCmd.Register(
chat.NewCmdChat(p), // picks up chat-feature middleware
deploy.NewCmdDeploy(p), // picks up deploy-feature middleware (if any)
)
return rootCmd
}
Equivalent and more common: pass children to the variadic constructor so the wiring is co-located with construction:
Either form works โ Register is what runs under the hood for both.
Avoid the raw cobra AddCommand
Calling rootCmd.Command.AddCommand(unwrappedCobraCmd) attaches a command without wrapping its RunE. The command runs without timing, recovery, or feature middleware. Always go through setup.Command.Register (or pass *setup.Command to the variadic root constructor).
Deprecated: setup.AddCommandWithMiddleware
The legacy setup.AddCommandWithMiddleware(parent, child, feature) helper is kept as a // Deprecated: shim that delegates to Register. It no longer recurses into descendants (the recursive re-wrap with the parent's feature was always wrong) and will be removed in v1.0. Migrate to parent.Register(child).
How it works under the hood¶
Command.Register does three things per child:
- If the child has a
RunE, replace it withsetup.Chain(child.Feature, child.RunE).Chainwraps with all registered global middleware first, then any middleware registered forchild.Feature. - Call the embedded
(*cobra.Command).AddCommandto splice the child into the cobra tree. - Leave the child's own
Registercalls (its grandchildren) untouched โ those wrap themselves with their own feature when they were constructed.
The result: every command in the tree is wrapped exactly once with its own feature, regardless of how deep the nesting goes.
See also¶
- Command Middleware System โ chain semantics, execution order.
- Adding Custom Commands โ full custom-command walkthrough.
- Writing Custom Middleware โ how to build your own.