feat: add json logger output (#1026)
* feat: add json logger output This will allow to format log output as json. This is helpful in cases where act is not executed on a 'local' machine. * refactor: use runner config Using the runner config to configure logging is cleaner. Co-authored-by: Casey Lee <cplee@nektos.com>
This commit is contained in:
parent
7d403b8307
commit
14c98015c5
|
@ -38,6 +38,7 @@ type Input struct {
|
||||||
autoRemove bool
|
autoRemove bool
|
||||||
artifactServerPath string
|
artifactServerPath string
|
||||||
artifactServerPort string
|
artifactServerPort string
|
||||||
|
jsonLogger bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Input) resolve(path string) string {
|
func (i *Input) resolve(path string) string {
|
||||||
|
|
|
@ -60,6 +60,7 @@ func Execute(ctx context.Context, version string) {
|
||||||
rootCmd.PersistentFlags().BoolVarP(&input.noWorkflowRecurse, "no-recurse", "", false, "Flag to disable running workflows from subdirectories of specified path in '--workflows'/'-W' flag")
|
rootCmd.PersistentFlags().BoolVarP(&input.noWorkflowRecurse, "no-recurse", "", false, "Flag to disable running workflows from subdirectories of specified path in '--workflows'/'-W' flag")
|
||||||
rootCmd.PersistentFlags().StringVarP(&input.workdir, "directory", "C", ".", "working directory")
|
rootCmd.PersistentFlags().StringVarP(&input.workdir, "directory", "C", ".", "working directory")
|
||||||
rootCmd.PersistentFlags().BoolP("verbose", "v", false, "verbose output")
|
rootCmd.PersistentFlags().BoolP("verbose", "v", false, "verbose output")
|
||||||
|
rootCmd.PersistentFlags().BoolVar(&input.jsonLogger, "json", false, "Output logs in json format")
|
||||||
rootCmd.PersistentFlags().BoolVarP(&input.noOutput, "quiet", "q", false, "disable logging of output from steps")
|
rootCmd.PersistentFlags().BoolVarP(&input.noOutput, "quiet", "q", false, "disable logging of output from steps")
|
||||||
rootCmd.PersistentFlags().BoolVarP(&input.dryrun, "dryrun", "n", false, "dryrun mode")
|
rootCmd.PersistentFlags().BoolVarP(&input.dryrun, "dryrun", "n", false, "dryrun mode")
|
||||||
rootCmd.PersistentFlags().StringVarP(&input.secretfile, "secret-file", "", ".secrets", "file with list of secrets to read from (e.g. --secret-file .secrets)")
|
rootCmd.PersistentFlags().StringVarP(&input.secretfile, "secret-file", "", ".secrets", "file with list of secrets to read from (e.g. --secret-file .secrets)")
|
||||||
|
@ -156,6 +157,10 @@ func readEnvs(path string, envs map[string]string) bool {
|
||||||
//nolint:gocyclo
|
//nolint:gocyclo
|
||||||
func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []string) error {
|
func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []string) error {
|
||||||
return func(cmd *cobra.Command, args []string) error {
|
return func(cmd *cobra.Command, args []string) error {
|
||||||
|
if input.jsonLogger {
|
||||||
|
log.SetFormatter(&log.JSONFormatter{})
|
||||||
|
}
|
||||||
|
|
||||||
if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" && input.containerArchitecture == "" {
|
if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" && input.containerArchitecture == "" {
|
||||||
l := log.New()
|
l := log.New()
|
||||||
l.SetFormatter(&log.TextFormatter{
|
l.SetFormatter(&log.TextFormatter{
|
||||||
|
@ -266,6 +271,7 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str
|
||||||
Workdir: input.Workdir(),
|
Workdir: input.Workdir(),
|
||||||
BindWorkdir: input.bindWorkdir,
|
BindWorkdir: input.bindWorkdir,
|
||||||
LogOutput: !input.noOutput,
|
LogOutput: !input.noOutput,
|
||||||
|
JSONLogger: input.jsonLogger,
|
||||||
Env: envs,
|
Env: envs,
|
||||||
Secrets: secrets,
|
Secrets: secrets,
|
||||||
InsecureSecrets: input.insecureSecrets,
|
InsecureSecrets: input.insecureSecrets,
|
||||||
|
|
|
@ -157,9 +157,14 @@ func TestAddmaskUsemask(t *testing.T) {
|
||||||
|
|
||||||
a := assert.New(t)
|
a := assert.New(t)
|
||||||
|
|
||||||
|
config := &Config{
|
||||||
|
Secrets: map[string]string{},
|
||||||
|
InsecureSecrets: false,
|
||||||
|
}
|
||||||
|
|
||||||
re := captureOutput(t, func() {
|
re := captureOutput(t, func() {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
ctx = WithJobLogger(ctx, "testjob", map[string]string{}, false, &rc.Masks)
|
ctx = WithJobLogger(ctx, "testjob", config, &rc.Masks)
|
||||||
|
|
||||||
handler := rc.commandHandler(ctx)
|
handler := rc.commandHandler(ctx)
|
||||||
handler("::add-mask::secret\n")
|
handler("::add-mask::secret\n")
|
||||||
|
|
|
@ -38,14 +38,22 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithJobLogger attaches a new logger to context that is aware of steps
|
// WithJobLogger attaches a new logger to context that is aware of steps
|
||||||
func WithJobLogger(ctx context.Context, jobName string, secrets map[string]string, insecureSecrets bool, masks *[]string) context.Context {
|
func WithJobLogger(ctx context.Context, jobName string, config *Config, masks *[]string) context.Context {
|
||||||
mux.Lock()
|
mux.Lock()
|
||||||
defer mux.Unlock()
|
defer mux.Unlock()
|
||||||
formatter := new(stepLogFormatter)
|
|
||||||
formatter.color = colors[nextColor%len(colors)]
|
var formatter logrus.Formatter
|
||||||
formatter.secrets = secrets
|
if config.JSONLogger {
|
||||||
formatter.insecureSecrets = insecureSecrets
|
formatter = &jobLogJSONFormatter{
|
||||||
formatter.masks = masks
|
formatter: &logrus.JSONFormatter{},
|
||||||
|
masker: valueMasker(config.InsecureSecrets, config.Secrets, masks),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
formatter = &jobLogFormatter{
|
||||||
|
color: colors[nextColor%len(colors)],
|
||||||
|
masker: valueMasker(config.InsecureSecrets, config.Secrets, masks),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
nextColor++
|
nextColor++
|
||||||
|
|
||||||
|
@ -64,30 +72,39 @@ func WithJobLogger(ctx context.Context, jobName string, secrets map[string]strin
|
||||||
return common.WithLogger(ctx, rtn)
|
return common.WithLogger(ctx, rtn)
|
||||||
}
|
}
|
||||||
|
|
||||||
type stepLogFormatter struct {
|
type entryProcessor func(entry *logrus.Entry) *logrus.Entry
|
||||||
color int
|
|
||||||
secrets map[string]string
|
func valueMasker(insecureSecrets bool, secrets map[string]string, masks *[]string) entryProcessor {
|
||||||
insecureSecrets bool
|
return func(entry *logrus.Entry) *logrus.Entry {
|
||||||
masks *[]string
|
if insecureSecrets {
|
||||||
|
return entry
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range secrets {
|
||||||
|
if v != "" {
|
||||||
|
entry.Message = strings.ReplaceAll(entry.Message, v, "***")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range *masks {
|
||||||
|
if v != "" {
|
||||||
|
entry.Message = strings.ReplaceAll(entry.Message, v, "***")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return entry
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *stepLogFormatter) Format(entry *logrus.Entry) ([]byte, error) {
|
type jobLogFormatter struct {
|
||||||
|
color int
|
||||||
|
masker entryProcessor
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *jobLogFormatter) Format(entry *logrus.Entry) ([]byte, error) {
|
||||||
b := &bytes.Buffer{}
|
b := &bytes.Buffer{}
|
||||||
|
|
||||||
// Replace any secrets in the entry if insecure-secrets flag is not used
|
entry = f.masker(entry)
|
||||||
if !f.insecureSecrets {
|
|
||||||
for _, v := range f.secrets {
|
|
||||||
if v != "" {
|
|
||||||
entry.Message = strings.ReplaceAll(entry.Message, v, "***")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, v := range *f.masks {
|
|
||||||
if v != "" {
|
|
||||||
entry.Message = strings.ReplaceAll(entry.Message, v, "***")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if f.isColored(entry) {
|
if f.isColored(entry) {
|
||||||
f.printColored(b, entry)
|
f.printColored(b, entry)
|
||||||
|
@ -99,7 +116,7 @@ func (f *stepLogFormatter) Format(entry *logrus.Entry) ([]byte, error) {
|
||||||
return b.Bytes(), nil
|
return b.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *stepLogFormatter) printColored(b *bytes.Buffer, entry *logrus.Entry) {
|
func (f *jobLogFormatter) printColored(b *bytes.Buffer, entry *logrus.Entry) {
|
||||||
entry.Message = strings.TrimSuffix(entry.Message, "\n")
|
entry.Message = strings.TrimSuffix(entry.Message, "\n")
|
||||||
jobName := entry.Data["job"]
|
jobName := entry.Data["job"]
|
||||||
|
|
||||||
|
@ -112,7 +129,7 @@ func (f *stepLogFormatter) printColored(b *bytes.Buffer, entry *logrus.Entry) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *stepLogFormatter) print(b *bytes.Buffer, entry *logrus.Entry) {
|
func (f *jobLogFormatter) print(b *bytes.Buffer, entry *logrus.Entry) {
|
||||||
entry.Message = strings.TrimSuffix(entry.Message, "\n")
|
entry.Message = strings.TrimSuffix(entry.Message, "\n")
|
||||||
jobName := entry.Data["job"]
|
jobName := entry.Data["job"]
|
||||||
|
|
||||||
|
@ -125,7 +142,7 @@ func (f *stepLogFormatter) print(b *bytes.Buffer, entry *logrus.Entry) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *stepLogFormatter) isColored(entry *logrus.Entry) bool {
|
func (f *jobLogFormatter) isColored(entry *logrus.Entry) bool {
|
||||||
isColored := checkIfTerminal(entry.Logger.Out)
|
isColored := checkIfTerminal(entry.Logger.Out)
|
||||||
|
|
||||||
if force, ok := os.LookupEnv("CLICOLOR_FORCE"); ok && force != "0" {
|
if force, ok := os.LookupEnv("CLICOLOR_FORCE"); ok && force != "0" {
|
||||||
|
@ -147,3 +164,12 @@ func checkIfTerminal(w io.Writer) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type jobLogJSONFormatter struct {
|
||||||
|
masker entryProcessor
|
||||||
|
formatter *logrus.JSONFormatter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *jobLogJSONFormatter) Format(entry *logrus.Entry) ([]byte, error) {
|
||||||
|
return f.formatter.Format(f.masker(entry))
|
||||||
|
}
|
||||||
|
|
|
@ -31,6 +31,7 @@ type Config struct {
|
||||||
ForcePull bool // force pulling of the image, even if already present
|
ForcePull bool // force pulling of the image, even if already present
|
||||||
ForceRebuild bool // force rebuilding local docker image action
|
ForceRebuild bool // force rebuilding local docker image action
|
||||||
LogOutput bool // log the output from docker run
|
LogOutput bool // log the output from docker run
|
||||||
|
JSONLogger bool // use json or text logger
|
||||||
Env map[string]string // env for containers
|
Env map[string]string // env for containers
|
||||||
Secrets map[string]string // list of secrets
|
Secrets map[string]string // list of secrets
|
||||||
InsecureSecrets bool // switch hiding output when printing to terminal
|
InsecureSecrets bool // switch hiding output when printing to terminal
|
||||||
|
@ -164,7 +165,7 @@ func (runner *runnerImpl) NewPlanExecutor(plan *model.Plan) common.Executor {
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})(common.WithJobErrorContainer(WithJobLogger(ctx, jobName, rc.Config.Secrets, rc.Config.InsecureSecrets, &rc.Masks)))
|
})(common.WithJobErrorContainer(WithJobLogger(ctx, jobName, rc.Config, &rc.Masks)))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
pipeline = append(pipeline, common.NewParallelExecutor(maxParallel, stageExecutor...))
|
pipeline = append(pipeline, common.NewParallelExecutor(maxParallel, stageExecutor...))
|
||||||
|
|
Loading…
Reference in New Issue