diff --git a/app/logger.go b/app/logger.go new file mode 100644 index 0000000..1a0104a --- /dev/null +++ b/app/logger.go @@ -0,0 +1,93 @@ +package app + +import ( + "context" + "fmt" + "io" + "log/slog" + "sync" +) + +const ( + colorRed = "\033[31m" + colorReset = "\033[0m" +) + +type ConsoleHandler struct { + mu *sync.Mutex // guards writes to out + stdout io.Writer + stderr io.Writer + handler slog.Handler +} + +// NewConsoleHandler creates a slog handler targeted for CLI interfaces being +// run in an interactive mode with a TTY. The console handler: +// - Passes slog.LevelDebug and slog.LevelWarning to the underlying handler +// to work as usual +// - Prints slog.LevelInfo messages to stdout in the most natural way it can +// - Prints slog.LevelError messages to stderr with an `ERROR:` prefix in red. +// +// Its basically a way to try and turn an API that was designed for structured +// logging inside a daemon to be more usable in a CLI application. +func NewConsoleHandler(h slog.Handler, stdout, stderr io.Writer) slog.Handler { + mu := &sync.Mutex{} + return &ConsoleHandler{ + stdout: stdout, + stderr: stderr, + mu: mu, + handler: h, + } +} + +func (h *ConsoleHandler) Enabled(ctx context.Context, level slog.Level) bool { + return h.handler.Enabled(ctx, level) +} + +func (h *ConsoleHandler) Handle(ctx context.Context, r slog.Record) error { + switch r.Level { + case slog.LevelError: + r.Message = fmt.Sprintf("%sERROR:%s %s", colorRed, colorReset, r.Message) + return h.print(h.stderr, r) + case slog.LevelInfo: + return h.print(h.stdout, r) + default: + return h.handler.Handle(ctx, r) + } +} + +func (h *ConsoleHandler) print(w io.Writer, r slog.Record) error { + h.mu.Lock() + defer h.mu.Unlock() + + fmt.Fprint(w, r.Message) + first := true + r.Attrs(func(a slog.Attr) bool { + if first { + fmt.Fprintf(w, ": %s=%s", a.Key, a.Value) + first = false + } else { + fmt.Fprintf(w, ", %s=%s", a.Key, a.Value) + } + return true + }) + fmt.Fprint(w, "\n") + return nil +} + +func (h *ConsoleHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + return &ConsoleHandler{ + stdout: h.stdout, + stderr: h.stderr, + mu: h.mu, + handler: h.handler.WithAttrs(attrs), + } +} + +func (h *ConsoleHandler) WithGroup(name string) slog.Handler { + return &ConsoleHandler{ + stdout: h.stdout, + stderr: h.stderr, + mu: h.mu, + handler: h.handler.WithGroup(name), + } +} diff --git a/go.mod b/go.mod index 145913c..357aedf 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/foundriesio/fioconfig -go 1.22 +go 1.22.0 require ( github.com/ThalesIgnite/crypto11 v1.2.5 @@ -12,6 +12,7 @@ require ( github.com/stretchr/testify v1.9.0 github.com/urfave/cli/v2 v2.27.1 go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 + golang.org/x/sys v0.30.0 ) require ( diff --git a/go.sum b/go.sum index c989e09..170123e 100644 --- a/go.sum +++ b/go.sum @@ -40,6 +40,8 @@ github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk= go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 h1:CCriYyAfq1Br1aIYettdHZTy8mBTIPo7We18TuO/bak= go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/internal/config.go b/internal/config.go index b749932..7e2a05a 100644 --- a/internal/config.go +++ b/internal/config.go @@ -33,7 +33,7 @@ func UnmarshallBuffer(c CryptoHandler, encContent []byte, decrypt bool) (ConfigS if decrypt { for fname, cfgFile := range config { if !cfgFile.Unencrypted { - slog.Info("Decoding value", "file", fname) + slog.Debug("Decoding value", "file", fname) decrypted, err := c.Decrypt(cfgFile.Value) if err != nil { return nil, fmt.Errorf("%s: %v", fname, err) diff --git a/internal/vpn.go b/internal/vpn.go index 98ec4b3..37f5897 100644 --- a/internal/vpn.go +++ b/internal/vpn.go @@ -53,7 +53,7 @@ func (v *vpnInitCallback) ConfigFiles(app *App) []ConfigFileReq { register := false if _, err := os.Stat(wgPriv); os.IsNotExist(err) { register = true - slog.Info("Wireguard private key does not exist, generating.", "path", wgPriv) + slog.Info("Wireguard private key does not exist, generating", "path", wgPriv) } else { register = vpnBugFix(app, app.StorageDir) } diff --git a/main.go b/main.go index b674990..6e22ecb 100644 --- a/main.go +++ b/main.go @@ -10,12 +10,26 @@ import ( "strings" "time" + "github.com/foundriesio/fioconfig/app" "github.com/foundriesio/fioconfig/internal" "github.com/foundriesio/fioconfig/sotatoml" "github.com/urfave/cli/v2" + "golang.org/x/sys/unix" ) +func isTerminal(fd *os.File) bool { + _, err := unix.IoctlGetTermios(int(fd.Fd()), unix.TCGETS) + return err == nil +} + func NewApp(c *cli.Context) (*internal.App, error) { + if isTerminal(os.Stderr) { + orig := slog.NewTextHandler(os.Stderr, nil) + handler := app.NewConsoleHandler(orig, os.Stdout, os.Stderr) + logger := slog.New(handler) + slog.SetDefault(logger) + } + app, err := internal.NewApp(c.StringSlice("config"), c.String("secrets-dir"), c.Bool("unsafe-handlers"), false) if err != nil { return nil, err @@ -60,7 +74,8 @@ func checkin(c *cli.Context) error { if err != nil { return err } - slog.Info("Checking in with server") + + slog.Info("Checking in with server ...") if err := app.CheckIn(); err != nil && !errors.Is(err, internal.NotModifiedError) { return err } diff --git a/transport/tls.go b/transport/tls.go index f136742..a91c688 100644 --- a/transport/tls.go +++ b/transport/tls.go @@ -63,4 +63,3 @@ func loadCertLocal(cfg *sotatoml.AppConfig) (tls.Certificate, error) { } return tls.LoadX509KeyPair(certFile, keyFile) } - diff --git a/transport/tls_no_pkcs11.go b/transport/tls_no_pkcs11.go index 0059f44..1e08904 100644 --- a/transport/tls_no_pkcs11.go +++ b/transport/tls_no_pkcs11.go @@ -10,7 +10,6 @@ import ( "github.com/foundriesio/fioconfig/sotatoml" ) - func loadCertPkcs11(cfg *sotatoml.AppConfig) (*interface{}, tls.Certificate, error) { fmt.Println("ERROR: PKCS#11 is not supported") os.Exit(1) diff --git a/transport/tls_pkcs11.go b/transport/tls_pkcs11.go index 19d0140..d8770cb 100644 --- a/transport/tls_pkcs11.go +++ b/transport/tls_pkcs11.go @@ -11,7 +11,6 @@ import ( "github.com/foundriesio/fioconfig/sotatoml" ) - func loadCertPkcs11(cfg *sotatoml.AppConfig) (*crypto11.Context, tls.Certificate, error) { module := cfg.Get("p11.module") pin := cfg.Get("p11.pass")