Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions changelog/v0.7.14/clicore.yaml
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

66 changes: 66 additions & 0 deletions clicore/README.md
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**

91 changes: 91 additions & 0 deletions clicore/cli_encoder.go
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
}
152 changes: 152 additions & 0 deletions clicore/cli_test.go
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},
}
21 changes: 21 additions & 0 deletions clicore/clicore_suite_test.go
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")
}

5 changes: 5 additions & 0 deletions clicore/constants/constants.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package constants
Copy link
Member

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

Copy link
Contributor Author

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


// 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"
9 changes: 9 additions & 0 deletions clicore/internal/clibufferpool/resource.go
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
)
Loading