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 FeatureID("chat").
setup.RegisterMiddleware(props.FeatureID("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).
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.