diff --git a/cobrautil.go b/cobrautil.go index 6be3e13..2bed329 100644 --- a/cobrautil.go +++ b/cobrautil.go @@ -53,7 +53,7 @@ func SyncViperPreRunE(prefix string) CobraRunFunc { // CobraRunFunc is the signature of cobra.Command RunFuncs. type CobraRunFunc func(cmd *cobra.Command, args []string) error -// RunFuncStack chains together a collection of CobraCommandFuncs into one. +// CommandStack chains together a collection of CobraCommandFuncs into one. func CommandStack(cmdfns ...CobraRunFunc) CobraRunFunc { return func(cmd *cobra.Command, args []string) error { for _, cmdfn := range cmdfns { @@ -65,7 +65,10 @@ func CommandStack(cmdfns ...CobraRunFunc) CobraRunFunc { } } -func prefixJoiner(prefix string) func(...string) string { +// PrefixJoiner joins a list of strings with the "-" separator, including the provided prefix string +// +// example: PrefixJoiner("hi")("how", "are", "you") = "hi-how-are-you" +func PrefixJoiner(prefix string) func(...string) string { return func(xs ...string) string { return stringz.Join("-", append([]string{prefix}, xs...)...) } diff --git a/example_test.go b/example_test.go index c3e01f4..d96d2b4 100644 --- a/example_test.go +++ b/example_test.go @@ -4,7 +4,8 @@ import ( "github.com/rs/zerolog" "github.com/spf13/cobra" - "github.com/jzelinskie/cobrautil" + "github.com/jzelinskie/cobrautil/v2" + zl "github.com/jzelinskie/cobrautil/v2/zerolog" ) func ExampleCommandStack() { @@ -12,27 +13,27 @@ func ExampleCommandStack() { Use: "mycmd", PreRunE: cobrautil.CommandStack( cobrautil.SyncViperPreRunE("myprogram"), - cobrautil.ZeroLogRunE("log", zerolog.InfoLevel), + zl.RunE("log", zerolog.InfoLevel), ), } - cobrautil.RegisterZeroLogFlags(cmd.PersistentFlags(), "log") + zl.RegisterZeroLogFlags(cmd.PersistentFlags(), "log") } func ExampleRegisterZeroLogFlags() { cmd := &cobra.Command{ Use: "mycmd", - PreRunE: cobrautil.ZeroLogRunE("log", zerolog.InfoLevel), + PreRunE: zl.RunE("log", zerolog.InfoLevel), } - cobrautil.RegisterZeroLogFlags(cmd.PersistentFlags(), "log") + zl.RegisterZeroLogFlags(cmd.PersistentFlags(), "log") } -func ExampleZeroLogRunE() { +func ExampleRunE() { cmd := &cobra.Command{ Use: "mycmd", - PreRunE: cobrautil.ZeroLogRunE("log", zerolog.InfoLevel), + PreRunE: zl.RunE("log", zerolog.InfoLevel), } - cobrautil.RegisterZeroLogFlags(cmd.PersistentFlags(), "log") + zl.RegisterZeroLogFlags(cmd.PersistentFlags(), "log") } diff --git a/go.mod b/go.mod index a62a8bf..9941b1f 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,9 @@ -module github.com/jzelinskie/cobrautil +module github.com/jzelinskie/cobrautil/v2 go 1.18 require ( + github.com/go-logr/logr v1.2.3 github.com/jzelinskie/stringz v0.0.1 github.com/mattn/go-isatty v0.0.16 github.com/rs/zerolog v1.28.0 @@ -23,7 +24,6 @@ require ( require ( github.com/cenkalti/backoff/v4 v4.1.3 // indirect github.com/fsnotify/fsnotify v1.5.4 // indirect - github.com/go-logr/logr v1.2.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 // indirect diff --git a/grpc.go b/grpc.go deleted file mode 100644 index 88c3fe4..0000000 --- a/grpc.go +++ /dev/null @@ -1,99 +0,0 @@ -package cobrautil - -import ( - "fmt" - "net" - "time" - - "github.com/jzelinskie/stringz" - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" - "github.com/spf13/cobra" - "github.com/spf13/pflag" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials" - "google.golang.org/grpc/keepalive" -) - -// RegisterGrpcServerFlags adds the following flags for use with -// GrpcServerFromFlags: -// - "$PREFIX-addr" -// - "$PREFIX-tls-cert-path" -// - "$PREFIX-tls-key-path" -// - "$PREFIX-max-conn-age" -func RegisterGrpcServerFlags(flags *pflag.FlagSet, flagPrefix, serviceName, defaultAddr string, defaultEnabled bool) { - serviceName = stringz.DefaultEmpty(serviceName, "grpc") - defaultAddr = stringz.DefaultEmpty(defaultAddr, ":50051") - prefixed := prefixJoiner(stringz.DefaultEmpty(flagPrefix, "grpc")) - - flags.String(prefixed("addr"), defaultAddr, "address to listen on to serve "+serviceName) - flags.String(prefixed("network"), "tcp", "network type to serve "+serviceName+` ("tcp", "tcp4", "tcp6", "unix", "unixpacket")`) - flags.String(prefixed("tls-cert-path"), "", "local path to the TLS certificate used to serve "+serviceName) - flags.String(prefixed("tls-key-path"), "", "local path to the TLS key used to serve "+serviceName) - flags.Duration(prefixed("max-conn-age"), 30*time.Second, "how long a connection serving "+serviceName+" should be able to live") - flags.Bool(prefixed("enabled"), defaultEnabled, "enable "+serviceName+" gRPC server") -} - -// GrpcServerFromFlags creates an *grpc.Server as configured by the flags from -// RegisterGrpcServerFlags(). -func GrpcServerFromFlags(cmd *cobra.Command, flagPrefix string, opts ...grpc.ServerOption) (*grpc.Server, error) { - prefixed := prefixJoiner(stringz.DefaultEmpty(flagPrefix, "grpc")) - - opts = append(opts, grpc.KeepaliveParams(keepalive.ServerParameters{ - MaxConnectionAge: MustGetDuration(cmd, prefixed("max-conn-age")), - })) - - certPath := MustGetStringExpanded(cmd, prefixed("tls-cert-path")) - keyPath := MustGetStringExpanded(cmd, prefixed("tls-key-path")) - - switch { - case certPath == "" && keyPath == "": - log.Warn().Str("prefix", flagPrefix).Msg("grpc server serving plaintext") - return grpc.NewServer(opts...), nil - - case certPath != "" && keyPath != "": - creds, err := credentials.NewServerTLSFromFile(certPath, keyPath) - if err != nil { - return nil, err - } - opts = append(opts, grpc.Creds(creds)) - return grpc.NewServer(opts...), nil - - default: - return nil, fmt.Errorf( - "failed to start gRPC server: must provide both --%s-tls-cert-path and --%s-tls-key-path", - flagPrefix, - flagPrefix, - ) - } -} - -// GrpcListenFromFlags listens on an gRPC server using the configuration stored -// in the cobra command that was registered with RegisterGrpcServerFlags. -func GrpcListenFromFlags(cmd *cobra.Command, flagPrefix string, srv *grpc.Server, level zerolog.Level) error { - prefixed := prefixJoiner(stringz.DefaultEmpty(flagPrefix, "grpc")) - - if !MustGetBool(cmd, prefixed("enabled")) { - return nil - } - - network := MustGetString(cmd, prefixed("network")) - addr := MustGetStringExpanded(cmd, prefixed("addr")) - - l, err := net.Listen(network, addr) - if err != nil { - return fmt.Errorf("failed to listen on addr for gRPC server: %w", err) - } - - log.WithLevel(level). - Str("addr", addr). - Str("network", network). - Str("prefix", flagPrefix). - Msg("grpc server started listening") - - if err := srv.Serve(l); err != nil { - return fmt.Errorf("failed to serve gRPC: %w", err) - } - - return nil -} diff --git a/grpc/grpc.go b/grpc/grpc.go new file mode 100644 index 0000000..08aae0a --- /dev/null +++ b/grpc/grpc.go @@ -0,0 +1,198 @@ +package grpc + +import ( + "fmt" + "net" + "time" + + "github.com/jzelinskie/cobrautil/v2" + + "github.com/go-logr/logr" + "github.com/jzelinskie/stringz" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/keepalive" +) + +// ConfigureFunc is a function used to configure this CobraUtil +type ConfigureFunc = func(cu *CobraUtil) + +// New creates a configuration that exposes RegisterFlags and RunE +// to integrate with cobra +func New(serviceName string, configurations ...ConfigureFunc) *CobraUtil { + cu := CobraUtil{ + serviceName: stringz.DefaultEmpty(serviceName, "grpc"), + preRunLevel: 0, + logger: logr.Discard(), + defaultAddr: ":50051", + defaultEnabled: false, + flagPrefix: "grpc", + } + for _, configure := range configurations { + configure(&cu) + } + return &cu +} + +// CobraUtil carries the configuration for a otel CobraRunFunc +type CobraUtil struct { + flagPrefix string + serviceName string + defaultAddr string + defaultEnabled bool + logger logr.Logger + preRunLevel int +} + +// RegisterGrpcServerFlags adds the following flags for use with +// GrpcServerFromFlags: +// - "$PREFIX-addr" +// - "$PREFIX-tls-cert-path" +// - "$PREFIX-tls-key-path" +// - "$PREFIX-max-conn-age" +func RegisterGrpcServerFlags(flags *pflag.FlagSet, flagPrefix, serviceName, defaultAddr string, defaultEnabled bool) { + serviceName = stringz.DefaultEmpty(serviceName, "grpc") + defaultAddr = stringz.DefaultEmpty(defaultAddr, ":50051") + prefixed := cobrautil.PrefixJoiner(stringz.DefaultEmpty(flagPrefix, "grpc")) + + flags.String(prefixed("addr"), defaultAddr, "address to listen on to serve "+serviceName) + flags.String(prefixed("network"), "tcp", "network type to serve "+serviceName+` ("tcp", "tcp4", "tcp6", "unix", "unixpacket")`) + flags.String(prefixed("tls-cert-path"), "", "local path to the TLS certificate used to serve "+serviceName) + flags.String(prefixed("tls-key-path"), "", "local path to the TLS key used to serve "+serviceName) + flags.Duration(prefixed("max-conn-age"), 30*time.Second, "how long a connection serving "+serviceName+" should be able to live") + flags.Bool(prefixed("enabled"), defaultEnabled, "enable "+serviceName+" gRPC server") +} + +// RegisterGrpcServerFlags adds the following flags for use with +// GrpcServerFromFlags: +// - "$PREFIX-addr" +// - "$PREFIX-tls-cert-path" +// - "$PREFIX-tls-key-path" +// - "$PREFIX-max-conn-age" +func (cu CobraUtil) RegisterGrpcServerFlags(flags *pflag.FlagSet) { + RegisterGrpcServerFlags(flags, cu.flagPrefix, cu.serviceName, cu.defaultAddr, cu.defaultEnabled) +} + +// ServerFromFlags creates an *grpc.Server as configured by the flags from +// RegisterGrpcServerFlags(). +func ServerFromFlags(cmd *cobra.Command, flagPrefix string, opts ...grpc.ServerOption) (*grpc.Server, error) { + return New("", WithFlagPrefix(flagPrefix)).ServerFromFlags(cmd, opts...) +} + +// ServerFromFlags creates an *grpc.Server as configured by the flags from +// RegisterGrpcServerFlags(). +func (cu CobraUtil) ServerFromFlags(cmd *cobra.Command, opts ...grpc.ServerOption) (*grpc.Server, error) { + prefixed := cobrautil.PrefixJoiner(cu.flagPrefix) + + opts = append(opts, grpc.KeepaliveParams(keepalive.ServerParameters{ + MaxConnectionAge: cobrautil.MustGetDuration(cmd, prefixed("max-conn-age")), + })) + + certPath := cobrautil.MustGetStringExpanded(cmd, prefixed("tls-cert-path")) + keyPath := cobrautil.MustGetStringExpanded(cmd, prefixed("tls-key-path")) + + switch { + case isInsecure(certPath, keyPath): + return grpc.NewServer(opts...), nil + + case isSecure(certPath, keyPath): + creds, err := credentials.NewServerTLSFromFile(certPath, keyPath) + if err != nil { + return nil, err + } + opts = append(opts, grpc.Creds(creds)) + return grpc.NewServer(opts...), nil + + default: + return nil, fmt.Errorf( + "failed to start gRPC server: must provide both --%s-tls-cert-path and --%s-tls-key-path", + cu.flagPrefix, + cu.flagPrefix, + ) + } +} + +// ListenFromFlags listens on an gRPC server using the configuration stored +// in the cobra command that was registered with RegisterGrpcServerFlags. +func ListenFromFlags(cmd *cobra.Command, flagPrefix string, srv *grpc.Server, preRunLevel int) error { + return New("", WithPreRunLevel(preRunLevel), WithFlagPrefix(flagPrefix)).ListenFromFlags(cmd, srv) +} + +// ListenFromFlags listens on an gRPC server using the configuration stored +// in the cobra command that was registered with RegisterGrpcServerFlags. +func (cu CobraUtil) ListenFromFlags(cmd *cobra.Command, srv *grpc.Server) error { + prefixed := cobrautil.PrefixJoiner(cu.flagPrefix) + + if !cobrautil.MustGetBool(cmd, prefixed("enabled")) { + return nil + } + + network := cobrautil.MustGetString(cmd, prefixed("network")) + addr := cobrautil.MustGetStringExpanded(cmd, prefixed("addr")) + + l, err := net.Listen(network, addr) + if err != nil { + return fmt.Errorf("failed to listen on addr for gRPC server: %w", err) + } + + certPath := cobrautil.MustGetStringExpanded(cmd, prefixed("tls-cert-path")) + keyPath := cobrautil.MustGetStringExpanded(cmd, prefixed("tls-key-path")) + cu.logger.V(cu.preRunLevel).Info( + "grpc server started listening", + "addr", addr, + "network", network, + "prefix", cu.flagPrefix, + "insecure", isInsecure(certPath, keyPath)) + + if err := srv.Serve(l); err != nil { + return fmt.Errorf("failed to serve gRPC: %w", err) + } + + return nil +} + +// WithLogger defines the logger used to log messages in this package +func WithLogger(logger logr.Logger) ConfigureFunc { + return func(cu *CobraUtil) { + cu.logger = logger + } +} + +// WithDefaultAddress defines the default value of the address the server will listen at. +// Defaults to ":50051" +func WithDefaultAddress(addr string) ConfigureFunc { + return func(cu *CobraUtil) { + cu.defaultAddr = addr + } +} + +// WithDefaultEnabled defines whether the gRPC server is enabled by default. Defaults to "false". +func WithDefaultEnabled(enabled bool) ConfigureFunc { + return func(cu *CobraUtil) { + cu.defaultEnabled = enabled + } +} + +// WithFlagPrefix defines prefix used with the generated flags. Defaults to "grpc". +func WithFlagPrefix(flagPrefix string) ConfigureFunc { + return func(cu *CobraUtil) { + cu.flagPrefix = flagPrefix + } +} + +// WithPreRunLevel defines the logging level used for pre-run log messages. Defaults to "debug". +func WithPreRunLevel(preRunLevel int) ConfigureFunc { + return func(cu *CobraUtil) { + cu.preRunLevel = preRunLevel + } +} + +func isInsecure(certPath, keyPath string) bool { + return certPath == "" && keyPath == "" +} + +func isSecure(certPath, keyPath string) bool { + return certPath != "" && keyPath != "" +} diff --git a/http.go b/http.go deleted file mode 100644 index a7bf71e..0000000 --- a/http.go +++ /dev/null @@ -1,75 +0,0 @@ -package cobrautil - -import ( - "errors" - "fmt" - "net/http" - - "github.com/jzelinskie/stringz" - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" - "github.com/spf13/cobra" - "github.com/spf13/pflag" -) - -// RegisterHTTPServerFlags adds the following flags for use with -// HttpServerFromFlags: -// - "$PREFIX-addr" -// - "$PREFIX-tls-cert-path" -// - "$PREFIX-tls-key-path" -// - "$PREFIX-enabled" -func RegisterHTTPServerFlags(flags *pflag.FlagSet, flagPrefix, serviceName, defaultAddr string, defaultEnabled bool) { - serviceName = stringz.DefaultEmpty(serviceName, "http") - defaultAddr = stringz.DefaultEmpty(defaultAddr, ":8443") - prefixed := prefixJoiner(stringz.DefaultEmpty(flagPrefix, "http")) - - flags.String(prefixed("addr"), defaultAddr, "address to listen on to serve "+serviceName) - flags.String(prefixed("tls-cert-path"), "", "local path to the TLS certificate used to serve "+serviceName) - flags.String(prefixed("tls-key-path"), "", "local path to the TLS key used to serve "+serviceName) - flags.Bool(prefixed("enabled"), defaultEnabled, "enable "+serviceName+" http server") -} - -// HTTPServerFromFlags creates an *http.Server as configured by the flags from -// RegisterHttpServerFlags(). -func HTTPServerFromFlags(cmd *cobra.Command, flagPrefix string) *http.Server { - prefixed := prefixJoiner(stringz.DefaultEmpty(flagPrefix, "http")) - - return &http.Server{ - Addr: MustGetStringExpanded(cmd, prefixed("addr")), - } -} - -// HTTPListenFromFlags listens on an HTTP server using the configuration stored -// in the cobra command that was registered with RegisterHttpServerFlags. -func HTTPListenFromFlags(cmd *cobra.Command, flagPrefix string, srv *http.Server, level zerolog.Level) error { - prefixed := prefixJoiner(stringz.DefaultEmpty(flagPrefix, "http")) - if !MustGetBool(cmd, prefixed("enabled")) { - return nil - } - - certPath := MustGetStringExpanded(cmd, prefixed("tls-cert-path")) - keyPath := MustGetStringExpanded(cmd, prefixed("tls-key-path")) - - switch { - case certPath == "" && keyPath == "": - log.Warn().Str("addr", srv.Addr).Str("prefix", flagPrefix).Msg("http server serving plaintext") - if err := srv.ListenAndServe(); err != nil && errors.Is(err, http.ErrServerClosed) { - return fmt.Errorf("failed while serving http: %w", err) - } - return nil - - case certPath != "" && keyPath != "": - log.WithLevel(level).Str("addr", srv.Addr).Str("prefix", flagPrefix).Msg("https server started serving") - if err := srv.ListenAndServeTLS(certPath, keyPath); err != nil && errors.Is(err, http.ErrServerClosed) { - return fmt.Errorf("failed while serving https: %w", err) - } - return nil - - default: - return fmt.Errorf( - "failed to start http server: must provide both --%s-tls-cert-path and --%s-tls-key-path", - flagPrefix, - flagPrefix, - ) - } -} diff --git a/http/http.go b/http/http.go new file mode 100644 index 0000000..b525d73 --- /dev/null +++ b/http/http.go @@ -0,0 +1,180 @@ +package http + +import ( + "errors" + "fmt" + "net/http" + + "github.com/go-logr/logr" + "github.com/jzelinskie/cobrautil/v2" + "github.com/jzelinskie/stringz" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +// ConfigureFunc is a function used to configure this CobraUtil +type ConfigureFunc = func(cu *CobraUtil) + +// New creates a configuration that exposes RegisterFlags and RunE +// to integrate with cobra +func New(serviceName string, configurations ...ConfigureFunc) *CobraUtil { + cu := CobraUtil{ + serviceName: stringz.DefaultEmpty(serviceName, "http"), + preRunLevel: 0, + logger: logr.Discard(), + defaultAddr: ":8443", + defaultEnabled: false, + flagPrefix: "http", + } + for _, configure := range configurations { + configure(&cu) + } + return &cu +} + +// CobraUtil carries the configuration for a otel CobraRunFunc +type CobraUtil struct { + flagPrefix string + serviceName string + defaultAddr string + defaultEnabled bool + logger logr.Logger + preRunLevel int + handler http.Handler +} + +// RegisterHTTPServerFlags adds the following flags for use with +// HttpServerFromFlags: +// - "$PREFIX-addr" +// - "$PREFIX-tls-cert-path" +// - "$PREFIX-tls-key-path" +// - "$PREFIX-enabled" +func RegisterHTTPServerFlags(flags *pflag.FlagSet, flagPrefix, serviceName, defaultAddr string, defaultEnabled bool) { + defaultAddr = stringz.DefaultEmpty(defaultAddr, ":8443") + prefixed := cobrautil.PrefixJoiner(stringz.DefaultEmpty(flagPrefix, "http")) + + flags.String(prefixed("addr"), defaultAddr, "address to listen on to serve "+serviceName) + flags.String(prefixed("tls-cert-path"), "", "local path to the TLS certificate used to serve "+serviceName) + flags.String(prefixed("tls-key-path"), "", "local path to the TLS key used to serve "+serviceName) + flags.Bool(prefixed("enabled"), defaultEnabled, "enable "+serviceName+" http server") +} + +// ServerFromFlags creates an *http.Server as configured by the flags from +// RegisterHttpServerFlags(). +func ServerFromFlags(cmd *cobra.Command, flagPrefix string) *http.Server { + return New("", WithFlagPrefix(flagPrefix)).ServerFromFlags(cmd) +} + +// ListenFromFlags listens on an HTTP server using the configuration stored +// in the cobra command that was registered with RegisterHttpServerFlags. +func ListenFromFlags(cmd *cobra.Command, flagPrefix string, srv *http.Server, preRunLevel int) error { + return New("", WithFlagPrefix(flagPrefix), WithPreRunLevel(preRunLevel)).ListenWithServerFromFlags(cmd, srv) +} + +// RegisterHTTPServerFlags adds the following flags for use with +// HttpServerFromFlags: +// - "$PREFIX-addr" +// - "$PREFIX-tls-cert-path" +// - "$PREFIX-tls-key-path" +// - "$PREFIX-enabled" +func (cu CobraUtil) RegisterHTTPServerFlags(flags *pflag.FlagSet) { + RegisterHTTPServerFlags(flags, cu.flagPrefix, cu.serviceName, cu.defaultAddr, cu.defaultEnabled) +} + +// ServerFromFlags creates an *http.Server as configured by the flags from +// RegisterHttpServerFlags(). +func (cu CobraUtil) ServerFromFlags(cmd *cobra.Command) *http.Server { + prefixed := cobrautil.PrefixJoiner(cu.flagPrefix) + + return &http.Server{ + Addr: cobrautil.MustGetStringExpanded(cmd, prefixed("addr")), + Handler: cu.handler, + } +} + +// ListenWithServerFromFlags listens on the provided HTTP server using the configuration stored +// in the cobra command that was registered with RegisterHttpServerFlags. +func (cu CobraUtil) ListenWithServerFromFlags(cmd *cobra.Command, srv *http.Server) error { + prefixed := cobrautil.PrefixJoiner(cu.flagPrefix) + if !cobrautil.MustGetBool(cmd, prefixed("enabled")) { + return nil + } + + certPath := cobrautil.MustGetStringExpanded(cmd, prefixed("tls-cert-path")) + keyPath := cobrautil.MustGetStringExpanded(cmd, prefixed("tls-key-path")) + + switch { + case certPath == "" && keyPath == "": + cu.logger.V(cu.preRunLevel).Info("http server started serving", + "addr", srv.Addr, + "prefix", cu.flagPrefix, + "scheme", "http", + "insecure", "true") + if err := srv.ListenAndServe(); err != nil && errors.Is(err, http.ErrServerClosed) { + return fmt.Errorf("failed while serving http: %w", err) + } + return nil + + case certPath != "" && keyPath != "": + cu.logger.V(cu.preRunLevel).Info("http server started serving", + "addr", srv.Addr, + "prefix", cu.flagPrefix, + "scheme", "https", + "insecure", "false") + if err := srv.ListenAndServeTLS(certPath, keyPath); err != nil && errors.Is(err, http.ErrServerClosed) { + return fmt.Errorf("failed while serving https: %w", err) + } + return nil + + default: + return fmt.Errorf( + "failed to start http server: must provide both --%s-tls-cert-path and --%s-tls-key-path", + cu.flagPrefix, + cu.flagPrefix, + ) + } +} + +// WithLogger defines the logger used to log messages in this package +func WithLogger(logger logr.Logger) ConfigureFunc { + return func(cu *CobraUtil) { + cu.logger = logger + } +} + +// WithDefaultAddress defines the default value of the address the server will listen at. +// Defaults to ":8443" +func WithDefaultAddress(addr string) ConfigureFunc { + return func(cu *CobraUtil) { + cu.defaultAddr = addr + } +} + +// WithDefaultEnabled defines whether the http server is enabled by default. Defaults to "false". +func WithDefaultEnabled(enabled bool) ConfigureFunc { + return func(cu *CobraUtil) { + cu.defaultEnabled = enabled + } +} + +// WithFlagPrefix defines prefix used with the generated flags. Defaults to "http". +func WithFlagPrefix(flagPrefix string) ConfigureFunc { + return func(cu *CobraUtil) { + cu.flagPrefix = flagPrefix + } +} + +// WithPreRunLevel defines the logging level used for pre-run log messages. Defaults to "debug".. +func WithPreRunLevel(preRunLevel int) ConfigureFunc { + return func(cu *CobraUtil) { + cu.preRunLevel = preRunLevel + } +} + +// WithHandler defines the HTTP server handler to inject in the http.Server in ServerFromFlags method. +// No handler is set by default. The value will be ignored in ListenFromFlags. +func WithHandler(handler http.Handler) ConfigureFunc { + return func(cu *CobraUtil) { + cu.handler = handler + } +} diff --git a/otel.go b/otel/otel.go similarity index 65% rename from otel.go rename to otel/otel.go index 6cf29e1..7b075fa 100644 --- a/otel.go +++ b/otel/otel.go @@ -1,4 +1,4 @@ -package cobrautil +package otel import ( "context" @@ -7,9 +7,9 @@ import ( "runtime/debug" "strings" + "github.com/go-logr/logr" + "github.com/jzelinskie/cobrautil/v2" "github.com/jzelinskie/stringz" - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" "github.com/spf13/cobra" "github.com/spf13/pflag" "go.opentelemetry.io/contrib/propagators/b3" @@ -25,6 +25,31 @@ import ( semconv "go.opentelemetry.io/otel/semconv/v1.7.0" ) +// ConfigureFunc is a function used to configure this CobraUtil +type ConfigureFunc = func(cu *CobraUtil) + +// New creates a configuration that exposes RegisterFlags and RunE +// to integrate with cobra +func New(serviceName string, configurations ...ConfigureFunc) *CobraUtil { + cu := CobraUtil{ + serviceName: serviceName, + preRunLevel: 0, + logger: logr.Discard(), + } + for _, configure := range configurations { + configure(&cu) + } + return &cu +} + +// CobraUtil carries the configuration for a otel CobraRunFunc +type CobraUtil struct { + flagPrefix string + serviceName string + logger logr.Logger + preRunLevel int +} + // RegisterOpenTelemetryFlags adds the following flags for use with // OpenTelemetryPreRunE: // - "$PREFIX-provider" @@ -33,7 +58,7 @@ import ( func RegisterOpenTelemetryFlags(flags *pflag.FlagSet, flagPrefix, serviceName string) { bi, _ := debug.ReadBuildInfo() serviceName = stringz.DefaultEmpty(serviceName, bi.Main.Path) - prefixed := prefixJoiner(stringz.DefaultEmpty(flagPrefix, "otel")) + prefixed := cobrautil.PrefixJoiner(stringz.DefaultEmpty(flagPrefix, "otel")) flags.String(prefixed("provider"), "none", `OpenTelemetry provider for tracing ("none", "jaeger, otlphttp", "otlpgrpc")`) flags.String(prefixed("endpoint"), "", "OpenTelemetry collector endpoint - the endpoint can also be set by using enviroment variables") @@ -56,19 +81,41 @@ func RegisterOpenTelemetryFlags(flags *pflag.FlagSet, flagPrefix, serviceName st // corresponding otel provider from a command. // // The required flags can be added to a command by using +// RegisterOpenTelemetryFlags() +func OpenTelemetryRunE(flagPrefix, serviceName string, preRunLevel int) cobrautil.CobraRunFunc { + return New(serviceName, WithFlagPrefix(flagPrefix), WithPreRunLevel(preRunLevel)).RunE() +} + +// RegisterFlags adds the following flags for use with +// OpenTelemetryPreRunE: +// - "$PREFIX-provider" +// - "$PREFIX-endpoint" +// - "$PREFIX-service-name" +func (cu CobraUtil) RegisterFlags(flags *pflag.FlagSet) { + RegisterOpenTelemetryFlags(flags, cu.flagPrefix, cu.serviceName) +} + +// RunE returns a Cobra run func that configures the +// corresponding otel provider from a command. +// +// The required flags can be added to a command by using // RegisterOpenTelemetryFlags(). -func OpenTelemetryRunE(flagPrefix string, prerunLevel zerolog.Level) CobraRunFunc { - prefixed := prefixJoiner(stringz.DefaultEmpty(flagPrefix, "otel")) +func (cu CobraUtil) RunE() cobrautil.CobraRunFunc { + prefixed := cobrautil.PrefixJoiner(stringz.DefaultEmpty(cu.flagPrefix, "otel")) return func(cmd *cobra.Command, args []string) error { - if IsBuiltinCommand(cmd) { + if cobrautil.IsBuiltinCommand(cmd) { return nil // No-op for builtins } - provider := strings.ToLower(MustGetString(cmd, prefixed("provider"))) - serviceName := MustGetString(cmd, prefixed("service-name")) - endpoint := MustGetString(cmd, prefixed("endpoint")) - insecure := MustGetBool(cmd, prefixed("insecure")) - propagators := strings.Split(MustGetString(cmd, prefixed("trace-propagator")), ",") + provider := strings.ToLower(cobrautil.MustGetString(cmd, prefixed("provider"))) + serviceName := cobrautil.MustGetString(cmd, prefixed("service-name")) + endpoint := cobrautil.MustGetString(cmd, prefixed("endpoint")) + insecure := cobrautil.MustGetBool(cmd, prefixed("insecure")) + propagators := strings.Split(cobrautil.MustGetString(cmd, prefixed("trace-propagator")), ",") + var noLogger logr.Logger + if cu.logger != noLogger { + otel.SetLogger(cu.logger) + } var exporter trace.SpanExporter var err error @@ -82,8 +129,8 @@ func OpenTelemetryRunE(flagPrefix string, prerunLevel zerolog.Level) CobraRunFun // Nothing. case "jaeger": // Legacy flags! Will eventually be dropped! - endpoint = stringz.DefaultEmpty(endpoint, MustGetString(cmd, "otel-jaeger-endpoint")) - serviceName = stringz.Default(serviceName, MustGetString(cmd, "otel-jaeger-service-name"), "", cmd.Flags().Lookup(prefixed("service-name")).DefValue) + endpoint = stringz.DefaultEmpty(endpoint, cobrautil.MustGetString(cmd, "otel-jaeger-endpoint")) + serviceName = stringz.Default(serviceName, cobrautil.MustGetString(cmd, "otel-jaeger-service-name"), "", cmd.Flags().Lookup(prefixed("service-name")).DefValue) var opts []jaeger.CollectorEndpointOption @@ -143,17 +190,36 @@ func OpenTelemetryRunE(flagPrefix string, prerunLevel zerolog.Level) CobraRunFun return fmt.Errorf("unknown tracing provider: %s", provider) } - log. - WithLevel(prerunLevel). - Str("provider", provider). - Str("endpoint", endpoint). - Str("service", serviceName). - Bool("insecure", insecure). - Msg("setup opentelemetry tracing") + cu.logger.V(cu.preRunLevel). + Info("configured opentelemetry tracing", + "provider", provider, + "endpoint", endpoint, + "service", serviceName, + "insecure", insecure) return nil } } +func WithLogger(logger logr.Logger) ConfigureFunc { + return func(cu *CobraUtil) { + cu.logger = logger + } +} + +// WithFlagPrefix defines prefix used with the generated flags. Defaults to "log". +func WithFlagPrefix(flagPrefix string) ConfigureFunc { + return func(cu *CobraUtil) { + cu.flagPrefix = flagPrefix + } +} + +// WithPreRunLevel defines the logging level used for pre-run log messages. Debug by default. +func WithPreRunLevel(preRunLevel int) ConfigureFunc { + return func(cu *CobraUtil) { + cu.preRunLevel = preRunLevel + } +} + func initOtelTracer(exporter trace.SpanExporter, serviceName string, propagators []string) error { res, err := resource.New( context.Background(), diff --git a/zerolog.go b/zerolog.go deleted file mode 100644 index 327f142..0000000 --- a/zerolog.go +++ /dev/null @@ -1,66 +0,0 @@ -package cobrautil - -import ( - "fmt" - "os" - "strings" - - "github.com/jzelinskie/stringz" - "github.com/mattn/go-isatty" - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" - "github.com/spf13/cobra" - "github.com/spf13/pflag" -) - -// RegisterZeroLogFlags adds flags for use in with ZeroLogPreRunE: -// - "$PREFIX-level" -// - "$PREFIX-format" -func RegisterZeroLogFlags(flags *pflag.FlagSet, flagPrefix string) { - prefixed := prefixJoiner(stringz.DefaultEmpty(flagPrefix, "log")) - flags.String(prefixed("level"), "info", `verbosity of logging ("trace", "debug", "info", "warn", "error")`) - flags.String(prefixed("format"), "auto", `format of logs ("auto", "console", "json")`) -} - -// ZeroLogRunE returns a Cobra run func that configures the corresponding -// log level from a command. -// -// The required flags can be added to a command by using -// RegisterLoggingPersistentFlags(). -func ZeroLogRunE(flagPrefix string, prerunLevel zerolog.Level) CobraRunFunc { - prefixed := prefixJoiner(stringz.DefaultEmpty(flagPrefix, "log")) - return func(cmd *cobra.Command, args []string) error { - if IsBuiltinCommand(cmd) { - return nil // No-op for builtins - } - - format := MustGetString(cmd, prefixed("format")) - if format == "console" || format == "auto" && isatty.IsTerminal(os.Stdout.Fd()) { - log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) - } - - level := strings.ToLower(MustGetString(cmd, prefixed("level"))) - switch level { - case "trace": - zerolog.SetGlobalLevel(zerolog.TraceLevel) - case "debug": - zerolog.SetGlobalLevel(zerolog.DebugLevel) - case "info": - zerolog.SetGlobalLevel(zerolog.InfoLevel) - case "warn": - zerolog.SetGlobalLevel(zerolog.WarnLevel) - case "error": - zerolog.SetGlobalLevel(zerolog.ErrorLevel) - case "fatal": - zerolog.SetGlobalLevel(zerolog.FatalLevel) - case "panic": - zerolog.SetGlobalLevel(zerolog.PanicLevel) - default: - return fmt.Errorf("unknown log level: %s", level) - } - - log.WithLevel(prerunLevel).Str("new level", level).Msg("set log level") - - return nil - } -} diff --git a/zerolog/zerolog.go b/zerolog/zerolog.go new file mode 100644 index 0000000..4ba35c9 --- /dev/null +++ b/zerolog/zerolog.go @@ -0,0 +1,164 @@ +package zerolog + +import ( + "fmt" + "io" + "os" + "strings" + "time" + + "github.com/jzelinskie/cobrautil/v2" + "github.com/jzelinskie/stringz" + "github.com/mattn/go-isatty" + "github.com/rs/zerolog" + "github.com/rs/zerolog/diode" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +// ConfigureFunc is a function used to configure this CobraUtil +type ConfigureFunc = func(cu *CobraUtil) + +// New creates a configuration that exposes RegisterZeroLogFlags and ZeroLogRunE +// to integrate with cobra +func New(configurations ...ConfigureFunc) *CobraUtil { + cu := CobraUtil{ + preRunLevel: zerolog.InfoLevel, + } + for _, configure := range configurations { + configure(&cu) + } + return &cu +} + +// CobraUtil carries the configuration for a zerolog CobraRunFunc +type CobraUtil struct { + flagPrefix string + target func(zerolog.Logger) + async bool + asyncSize int + asyncPollInterval time.Duration + preRunLevel zerolog.Level +} + +// RegisterZeroLogFlags adds flags for use in with ZeroLogPreRunE: +// - "$PREFIX-level" +// - "$PREFIX-format" +func RegisterZeroLogFlags(flags *pflag.FlagSet, flagPrefix string) { + prefixed := cobrautil.PrefixJoiner(stringz.DefaultEmpty(flagPrefix, "log")) + flags.String(prefixed("level"), "info", `verbosity of logging ("trace", "debug", "info", "warn", "error")`) + flags.String(prefixed("format"), "auto", `format of logs ("auto", "console", "json")`) +} + +// RunE returns a Cobra run func that configures the corresponding +// log level from a command. +// +// The required flags can be added to a command by using +// RegisterLoggingPersistentFlags(). +func RunE(flagPrefix string, prerunLevel zerolog.Level) cobrautil.CobraRunFunc { + return New(WithFlagPrefix(flagPrefix), WithPreRunLevel(prerunLevel)).RunE() +} + +// RegisterFlags adds flags for use in with ZeroLogPreRunE: +// - "$PREFIX-level" +// - "$PREFIX-format" +func (cu CobraUtil) RegisterFlags(flags *pflag.FlagSet) { + RegisterZeroLogFlags(flags, cu.flagPrefix) +} + +// RunE returns a Cobra run func that configures the corresponding +// log level from a command. +// +// The required flags can be added to a command by using +// RegisterLoggingPersistentFlags(). +func (cu CobraUtil) RunE() cobrautil.CobraRunFunc { + prefixed := cobrautil.PrefixJoiner(stringz.DefaultEmpty(cu.flagPrefix, "log")) + return func(cmd *cobra.Command, args []string) error { + if cobrautil.IsBuiltinCommand(cmd) { + return nil // No-op for builtins + } + + var output io.Writer + + format := cobrautil.MustGetString(cmd, prefixed("format")) + if format == "console" || format == "auto" && isatty.IsTerminal(os.Stdout.Fd()) { + output = zerolog.ConsoleWriter{Out: os.Stderr} + } else { + output = os.Stderr + } + + if cu.async { + output = diode.NewWriter(output, 1000, 10*time.Millisecond, func(missed int) { + fmt.Printf("Logger Dropped %d messages", missed) + }) + } + + l := zerolog.New(output).With().Timestamp().Logger() + + level := strings.ToLower(cobrautil.MustGetString(cmd, prefixed("level"))) + switch level { + case "trace": + l = l.Level(zerolog.TraceLevel) + case "debug": + l = l.Level(zerolog.DebugLevel) + case "info": + l = l.Level(zerolog.InfoLevel) + case "warn": + l = l.Level(zerolog.WarnLevel) + case "error": + l = l.Level(zerolog.ErrorLevel) + case "fatal": + l = l.Level(zerolog.FatalLevel) + case "panic": + l = l.Level(zerolog.PanicLevel) + default: + return fmt.Errorf("unknown log level: %s", level) + } + + if cu.target != nil { + cu.target(l) + } else { + log.Logger = l + } + + l.WithLevel(cu.preRunLevel). + Str("format", format). + Str("log_level", level). + Str("provider", "zerolog"). + Bool("async", cu.async). + Msg("configured logging") + return nil + } +} + +// WithFlagPrefix defines prefix used with the generated flags. Defaults to "log". +func WithFlagPrefix(flagPrefix string) ConfigureFunc { + return func(cu *CobraUtil) { + cu.flagPrefix = flagPrefix + } +} + +// WithPreRunLevel defines the logging level used for pre-run log messages. Debug by default. +func WithPreRunLevel(preRunLevel zerolog.Level) ConfigureFunc { + return func(cu *CobraUtil) { + cu.preRunLevel = preRunLevel + } +} + +// WithAsync enables non-blocking logging. Size of the buffer and polling interval can be configured. +// Disabled by default. +func WithAsync(size int, pollInterval time.Duration) ConfigureFunc { + return func(cu *CobraUtil) { + cu.async = true + cu.asyncSize = size + cu.asyncPollInterval = pollInterval + } +} + +// WithTarget callback that forwards the configured logger. Useful when we want to keep it in a global variable. +func WithTarget(fn func(zerolog.Logger)) ConfigureFunc { + return func(cu *CobraUtil) { + cu.target = fn + } +}