Module 7 · Phase 4 · ~90 min
Polish: rebuild the entrypoint with cobra, add proper subcommand help, structured logging, graceful shutdown, optional config file, and a Makefile. Make this something you'd hand off without embarrassment.
-ldflags "-X main.version=...").--log-format json|text flag and a --log-level flag.go get github.com/spf13/cobra@latest
go mod tidy
cantonctl-starter/
├── cmd/cantonctl/
│ ├── main.go # thin: instantiates root command, calls Execute()
│ ├── root.go # root command + persistent flags
│ ├── ping.go # `ping` subcommand
│ ├── submit.go # `submit` subcommand
│ └── stream.go # `stream` subcommand
└── internal/ledger/
└── client.go # unchanged from phases 1-3
package main
import (
"context"
"log/slog"
"os"
"os/signal"
"syscall"
"github.com/spf13/cobra"
)
var (
// Persistent flags — bound on root, available to all subcommands
flagEndpoint string
flagTLS bool
flagCAFile string
flagToken string
flagLogLevel string
)
var rootCmd = &cobra.Command{
Use: "cantonctl",
Short: "Talk to a Canton participant's Ledger API.",
Long: `cantonctl is a small CLI for poking a Canton participant: ping, submit
commands, stream transaction updates. Use --help on any subcommand for flags.`,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
configureLogger()
return nil
},
SilenceUsage: true, // don't reprint help on err
}
func init() {
rootCmd.PersistentFlags().StringVar(&flagEndpoint, "endpoint", "localhost:5011", "Ledger API endpoint")
rootCmd.PersistentFlags().BoolVar(&flagTLS, "tls", false, "use TLS")
rootCmd.PersistentFlags().StringVar(&flagCAFile, "ca", "", "PEM CA bundle (with --tls)")
rootCmd.PersistentFlags().StringVar(&flagToken, "token", "", "Bearer JWT for the Authorization header")
rootCmd.PersistentFlags().StringVar(&flagLogLevel, "log-level", "info", "debug, info, warn, error")
}
func main() {
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
if err := rootCmd.ExecuteContext(ctx); err != nil {
slog.Error("command failed", "err", err)
os.Exit(1)
}
}
func configureLogger() {
var level slog.Level
level.UnmarshalText([]byte(flagLogLevel))
handler := slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: level})
slog.SetDefault(slog.New(handler))
}
package main
import (
"github.com/spf13/cobra"
"example.com/cantonctl/internal/ledger"
)
func init() {
rootCmd.AddCommand(&cobra.Command{
Use: "ping",
Short: "Health-check the participant",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
c, err := ledger.Dial(ctx, ledger.Config{
Endpoint: flagEndpoint,
TLS: flagTLS,
Token: flagToken,
})
if err != nil { return err }
defer c.Close()
return c.Ping(ctx)
},
})
}
Same shape for submit.go and stream.go, with their own flags. Each subcommand stays self-contained — the file structure scales as you add more.
VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo dev)
.PHONY: build test lint clean
build:
go build -ldflags "-X main.version=$(VERSION)" -o bin/cantonctl ./cmd/cantonctl
test:
go test -race ./...
lint:
go vet ./...
clean:
rm -rf bin/
./bin/cantonctl --help prints a coherent description and lists subcommands../bin/cantonctl ping --help shows endpoint/tls/log-level flags with defaults.stream exits within ~1 second with an "exiting" log line — no panic, no goroutine dump.go vet ./... passes.go test ./... passes (write a few unit tests for the ledger package — at minimum, that Dial validates required Config fields).If you have time, wire in:
--metrics-addr flag that starts a Prometheus exporter on a separate port and registers counters/histograms for submits and stream events.otelgrpc; configure an OTLP exporter to wherever you have a backend).--config flag that reads a YAML file via Viper.None of these are required. The base CLI is the milestone.
You can make build and the resulting binary supports ping, submit, and stream against a real Canton sandbox, with clean help text, structured logs, and graceful shutdown. Congratulations — you've built a Canton-aware Go CLI from scratch, which is exactly the kind of thing tooling-focused contributors ship.