-
Couldn't load subscription status.
- Fork 22
extract clilog from gs #134
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
05bc83f
extract clilog from gs
ae31d9d
changelog
cdc2e46
migrate updates
974e2ba
begin moving tests
d22edc0
move contextutils and fail handler
6ff0d36
changelog
8bf4c26
add readmes
36d401b
Merge branch 'master' into clicore
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| changelog: | ||
| - type: NEW_FEATURE | ||
| description: > | ||
| Introduce a utility for managing zap logs from a CLI tool. The new `clicore` | ||
| library provides a means of sending human-friendly log messages to the console | ||
| while capturing full json-formatted logs to a file. Library includes a pair of | ||
| `Run` methods for siplified execution of the "main" file and simplified output | ||
| validation during integration tests. | ||
| issueLink: https://github.com/solo-io/go-utils/issues/135 | ||
| - type: NEW_FEATURE | ||
| description: Simplified way to call cobra commands from test environments. | ||
| issueLink: https://github.com/solo-io/go-utils/issues/124 | ||
| - type: NEW_FEATURE | ||
| description: > | ||
| The `PrintTrimmedStack` fail handler simplifies error tracking in ginkgo tests | ||
| by printing a condensed stack trace upon failure. Printout excludes well-known | ||
| overhead files so you can more easily sight the failing line. This eliminates | ||
| the need to count stack offset via `ExpectWithOffset`. You can just use | ||
| `Expect`. | ||
| issueLink: https://github.com/solo-io/go-utils/issues/131 | ||
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| # CLI Core | ||
| - A library for feature-rich, configurable, and cross-project consistent command line interfaces. | ||
|
|
||
| ## Usage | ||
| - To use clicore, just define a `CommandConfig` and call `config.Run()`. | ||
| ### Example | ||
| - Sample `main` package, importing your cli: | ||
| ```go | ||
| package main | ||
| import ( | ||
| "myrepo/pkg/mycli" | ||
| ) | ||
| func main() { | ||
| mycli.MyCommandConfig.Run() | ||
| } | ||
| ``` | ||
|
|
||
| - Sample package, defining your CLI library and exporting the `CommandConfig`: | ||
|
|
||
| ```go | ||
| package mycli | ||
|
|
||
| import "github.com/solo-io/go-utils/clicore" | ||
|
|
||
| var MyCommandConfig = clicore.CommandConfig{ | ||
| Command: App, | ||
| Version: version.Version, | ||
| FileLogPathElements: FileLogPathElements, | ||
| OutputModeEnvVar: OutputModeEnvVar, | ||
| RootErrorMessage: ErrorMessagePreamble, | ||
| LoggingContext: []interface{}{"version", version.Version}, | ||
| } | ||
| ``` | ||
|
|
||
| ### How to write logs to the console and log file | ||
|
|
||
| There are three helpers that you can use: | ||
|
|
||
| ```go | ||
|
|
||
| contextutils.CliLogInfow(ctx, "this info log goes to file and console") | ||
| contextutils.CliLogWarnw(ctx, "this warn log goes to file and console") | ||
| contextutils.CliLogErrorw(ctx, "this error log goes to file and console") | ||
| ``` | ||
|
|
||
| Key-value pairs are supported too: | ||
|
|
||
| ```go | ||
| contextutils.CliLogInfow(ctx, "this infow log should go to file and console", | ||
| "extrakey1", "val1") | ||
| ``` | ||
|
|
||
| Which is equivalent to the longer form: | ||
|
|
||
| ```go | ||
| contextutils.LoggerFrom(ctx).Infow("message going to file only", | ||
| zap.String("cli", "info that will go to the console and file", | ||
| "extrakey1", "val1") | ||
| ``` | ||
|
|
||
| ## Usage in tests | ||
| - `clicore` was designed to simplify CLI specification and testing. | ||
| - To run `clicore` in test mode, call `cliOutput, err := cli.GlooshotConfig.RunForTest(args)`. | ||
| - Output from the command (stdout, stderr, and any log files) can then be validated one by one. | ||
| - **See the [test file](cli_test.go) for an example** | ||
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| package clicore | ||
|
|
||
| import ( | ||
| "sync" | ||
| "time" | ||
|
|
||
| "github.com/solo-io/go-utils/clicore/internal/clibufferpool" | ||
|
|
||
| "go.uber.org/zap/zapcore" | ||
|
|
||
| "go.uber.org/zap/buffer" | ||
| ) | ||
|
|
||
| // Unlike zap's built-in console encoder, this encoder just prints strings, in the manner of fmt.Println. | ||
| type cliEncoder struct { | ||
| buf *buffer.Buffer | ||
| printedKey string | ||
| } | ||
|
|
||
| // these interface methods are irrelevant to the current needs of the CLI encoder | ||
| func (c *cliEncoder) AddArray(key string, marshaler zapcore.ArrayMarshaler) error { return nil } | ||
| func (c *cliEncoder) AddObject(key string, marshaler zapcore.ObjectMarshaler) error { return nil } | ||
| func (c *cliEncoder) AddBinary(key string, value []byte) {} | ||
| func (c *cliEncoder) AddByteString(key string, value []byte) {} | ||
| func (c *cliEncoder) AddBool(key string, value bool) {} | ||
| func (c *cliEncoder) AddComplex128(key string, value complex128) {} | ||
| func (c *cliEncoder) AddComplex64(key string, value complex64) {} | ||
| func (c *cliEncoder) AddDuration(key string, value time.Duration) {} | ||
| func (c *cliEncoder) AddFloat64(key string, value float64) {} | ||
| func (c *cliEncoder) AddFloat32(key string, value float32) {} | ||
| func (c *cliEncoder) AddInt(key string, value int) {} | ||
| func (c *cliEncoder) AddInt64(key string, value int64) {} | ||
| func (c *cliEncoder) AddInt32(key string, value int32) {} | ||
| func (c *cliEncoder) AddInt16(key string, value int16) {} | ||
| func (c *cliEncoder) AddInt8(key string, value int8) {} | ||
| func (c *cliEncoder) AddTime(key string, value time.Time) {} | ||
| func (c *cliEncoder) AddString(key, val string) {} | ||
| func (c *cliEncoder) AddUint(key string, value uint) {} | ||
| func (c *cliEncoder) AddUint64(key string, value uint64) {} | ||
| func (c *cliEncoder) AddUint32(key string, value uint32) {} | ||
| func (c *cliEncoder) AddUint16(key string, value uint16) {} | ||
| func (c *cliEncoder) AddUint8(key string, value uint8) {} | ||
| func (c *cliEncoder) AddUintptr(key string, value uintptr) {} | ||
| func (c *cliEncoder) AddReflected(key string, value interface{}) error { return nil } | ||
| func (c *cliEncoder) OpenNamespace(key string) {} | ||
|
|
||
| func NewCliEncoder(printedKey string) zapcore.Encoder { | ||
| return &cliEncoder{ | ||
| printedKey: printedKey, | ||
| buf: clibufferpool.Get(), | ||
| } | ||
| } | ||
|
|
||
| var _encoderPool = sync.Pool{New: func() interface{} { | ||
| return &cliEncoder{} | ||
| }} | ||
|
|
||
| func getCliEncoder() *cliEncoder { | ||
| return _encoderPool.Get().(*cliEncoder) | ||
| } | ||
|
|
||
| func putCliEncoder(c *cliEncoder) { | ||
| c.buf = nil | ||
| c.printedKey = "" | ||
| _encoderPool.Put(c) | ||
| } | ||
|
|
||
| func (c cliEncoder) Clone() zapcore.Encoder { | ||
| clone := c.clone() | ||
| clone.buf.Write(c.buf.Bytes()) | ||
| return clone | ||
| } | ||
| func (c *cliEncoder) clone() *cliEncoder { | ||
| clone := getCliEncoder() | ||
| clone.buf = clibufferpool.Get() | ||
| clone.printedKey = c.printedKey | ||
| return clone | ||
| } | ||
|
|
||
| //EncodeEntry implements the distinguishing features of this encoder type. | ||
| func (c cliEncoder) EncodeEntry(ent zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) { | ||
| final := c.clone() | ||
| for _, f := range fields { | ||
| if f.Key == c.printedKey && f.String != "" { | ||
| final.buf.AppendString(f.String + "\n") | ||
| } | ||
| } | ||
| ret := final.buf | ||
| putCliEncoder(final) | ||
| return ret, nil | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,152 @@ | ||
| package clicore | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| . "github.com/onsi/ginkgo" | ||
| . "github.com/onsi/gomega" | ||
| "github.com/solo-io/go-utils/contextutils" | ||
| "github.com/spf13/cobra" | ||
| ) | ||
|
|
||
| var _ = Describe("Clicore", func() { | ||
|
|
||
| var standardCobraHelpBlockMatcher = MatchRegexp("Available Commands:") | ||
|
|
||
| Context("basic args and flags", func() { | ||
| It("should return help messages without error", func() { | ||
| _, _, err := appWithSimpleOutput("-h") | ||
| Expect(err).NotTo(HaveOccurred()) | ||
| _, _, err = appWithSimpleOutput("help") | ||
| Expect(err).NotTo(HaveOccurred()) | ||
| _, _, err = appWithSimpleOutput("--help") | ||
| Expect(err).NotTo(HaveOccurred()) | ||
| }) | ||
| }) | ||
|
|
||
| Context("expect human-friendly errors", func() { | ||
| It("should return human-friendly errors on bad input", func() { | ||
| cliOut := appWithLoggerOutput("--h") | ||
| Expect(cliOut.CobraStdout).To(Equal("")) | ||
| Expect(cliOut.CobraStderr).To(standardCobraHelpBlockMatcher) | ||
| // logs are not used in this code path so they should be empty | ||
| Expect(cliOut.LoggerConsoleStout).To(Equal("")) | ||
| Expect(cliOut.LoggerConsoleStderr).To(Equal("")) | ||
| }) | ||
| }) | ||
|
|
||
| Context("expect human-friendly logs", func() { | ||
| It("should return human-friendly errors on bad input", func() { | ||
| cliOut := appWithLoggerOutput("--flag1") | ||
| Expect(cliOut.CobraStdout). | ||
| To(Equal("cobra says 'hisssss' - but he should leave the console logs to the CliLog* utils.")) | ||
| Expect(cliOut.CobraStderr). | ||
| To(MatchRegexp("Error: cobra says 'hisssss' again - it's ok because this is a passed error")) | ||
| Expect(cliOut.CobraStderr). | ||
| To(standardCobraHelpBlockMatcher) | ||
| Expect(cliOut.LoggerConsoleStout). | ||
| To(Equal(`this info log should go to file and console | ||
| this warn log should go to file and console | ||
| this infow log should go to file and console | ||
| this warnw log should go to file and console | ||
| `)) | ||
| Expect(cliOut.LoggerConsoleStderr).To(Equal(`this error log should go to file and console | ||
| this errorw log should go to file and console | ||
| `)) | ||
| // match the tags that are part of the rich log output | ||
| Expect(cliOut.LoggerFileContent).To(MatchRegexp("level")) | ||
| Expect(cliOut.LoggerFileContent).To(MatchRegexp("ts")) | ||
| Expect(cliOut.LoggerFileContent).To(MatchRegexp("warn")) | ||
| Expect(cliOut.LoggerFileContent).To(MatchRegexp("error")) | ||
| Expect(cliOut.LoggerFileContent).To(MatchRegexp(appVersion)) | ||
| Expect(cliOut.LoggerFileContent).To(MatchRegexp("msg")) | ||
| Expect(cliOut.LoggerFileContent).To(MatchRegexp("logger")) | ||
| // match (or not) the fragments that we get in the console. Using regex since timestamp is random | ||
| // see sampleLogFileContent for an example of the full output | ||
| Expect(cliOut.LoggerFileContent).NotTo(MatchRegexp("CliLog* utils")) | ||
| Expect(cliOut.LoggerFileContent).To(MatchRegexp("ok because this is a passed error")) | ||
| Expect(cliOut.LoggerFileContent).To(MatchRegexp("info log")) | ||
| Expect(cliOut.LoggerFileContent).To(MatchRegexp("warn log")) | ||
| Expect(cliOut.LoggerFileContent).To(MatchRegexp("error log")) | ||
| for i := 1; i <= 3; i++ { | ||
| Expect(cliOut.LoggerFileContent).To(MatchRegexp(fmt.Sprintf("extrakey%v", i))) | ||
| Expect(cliOut.LoggerFileContent).To(MatchRegexp(fmt.Sprintf("val%v", i))) | ||
| } | ||
|
|
||
| }) | ||
|
|
||
| }) | ||
| }) | ||
|
|
||
| func appWithSimpleOutput(args string) (string, string, error) { | ||
| co := appWithLoggerOutput(args) | ||
| return co.CobraStdout, co.CobraStderr, nil | ||
| } | ||
|
|
||
| // This is all you need to do to use the cli logger in a test environment | ||
| func appWithLoggerOutput(args string) CliOutput { | ||
| cliOutput, err := sampleAppConfig.RunForTest(args) | ||
| Expect(err).NotTo(HaveOccurred()) | ||
| return cliOutput | ||
| } | ||
|
|
||
| var ( | ||
| appVersion = "test" | ||
| fileLogPathElements = []string{".sample", "log", "dir"} | ||
| outputModeEnvVar = "SET_OUTPUT_MODE" | ||
| errorMessagePreamble = "error running cli" | ||
| ) | ||
|
|
||
| type TopOptions struct { | ||
| Flag1 bool | ||
| } | ||
|
|
||
| func SampleCobraCli(ctx context.Context, version string) *cobra.Command { | ||
| o := TopOptions{} | ||
| app := &cobra.Command{ | ||
| Use: "samplecli", | ||
| Short: "sample CLI for testing", | ||
| Version: version, | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| if o.Flag1 { | ||
| // Trigger some warnings, this will be removed | ||
| contextutils.CliLogInfow(ctx, "this info log should go to file and console") | ||
| contextutils.CliLogWarnw(ctx, "this warn log should go to file and console") | ||
| contextutils.CliLogErrorw(ctx, "this error log should go to file and console") | ||
| contextutils.CliLogInfow(ctx, "this infow log should go to file and console", "extrakey1", "val1") | ||
| contextutils.CliLogWarnw(ctx, "this warnw log should go to file and console", "extrakey2", "val2") | ||
| contextutils.CliLogErrorw(ctx, "this errorw log should go to file and console", "extrakey3", "val3") | ||
| fmt.Println("cobra says 'hisssss' - but he should leave the console logs to the CliLog* utils.") | ||
| return fmt.Errorf("cobra says 'hisssss' again - it's ok because this is a passed error") | ||
| } | ||
| return nil | ||
| }, | ||
| } | ||
|
|
||
| app.AddCommand( | ||
| sampleSubCommand(o), | ||
| ) | ||
|
|
||
| pflags := app.PersistentFlags() | ||
| pflags.BoolVar(&o.Flag1, "flag1", false, "this is a dummy flag to trigger logging") | ||
| return app | ||
| } | ||
|
|
||
| func sampleSubCommand(o TopOptions) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "subcmd1", | ||
| Short: "just a sample sub command", | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| return nil | ||
| }, | ||
| } | ||
| return cmd | ||
| } | ||
| var sampleAppConfig = CommandConfig{ | ||
| Command: SampleCobraCli, | ||
| Version: appVersion, | ||
| FileLogPathElements: fileLogPathElements, | ||
| OutputModeEnvVar: outputModeEnvVar, | ||
| RootErrorMessage: errorMessagePreamble, | ||
| LoggingContext: []interface{}{"version", appVersion}, | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| package clicore | ||
|
|
||
| import ( | ||
| . "github.com/onsi/ginkgo" | ||
| . "github.com/onsi/gomega" | ||
| "github.com/solo-io/go-utils/testutils" | ||
| "testing" | ||
| ) | ||
|
|
||
| func TestCliCore(t *testing.T) { | ||
|
|
||
| testutils.RegisterPreFailHandler( | ||
| func() { | ||
| testutils.PrintTrimmedStack() | ||
| }) | ||
| testutils.RegisterCommonFailHandlers() | ||
| RegisterFailHandler(Fail) | ||
| testutils.SetupLog() | ||
| RunSpecs(t, "Clicore Suite") | ||
| } | ||
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| package constants | ||
|
|
||
| // CliLoggerKey is the key passed through zap logs that indicates that its value should be written to the console, | ||
| // in addition to the full log file. | ||
| const CliLoggerKey = "cli" | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| package clibufferpool | ||
|
|
||
| import "go.uber.org/zap/buffer" | ||
|
|
||
| var ( | ||
| _pool = buffer.NewPool() | ||
| // Get retrieves a buffer from the pool, creating one if necessary. | ||
| Get = _pool.Get | ||
| ) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm wondering why this is in an entirely different package. I get that it is a constant but it's completely tied to the clicore main package and if I were a user of this package I would definitely look there first
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@EItanya there was an import cycle between clicore and contextutils