diff --git a/cmd/input.go b/cmd/input.go index 4dea829..e7f48df 100644 --- a/cmd/input.go +++ b/cmd/input.go @@ -38,6 +38,7 @@ type Input struct { autoRemove bool artifactServerPath string artifactServerPort string + jsonLogger bool } func (i *Input) resolve(path string) string { diff --git a/cmd/root.go b/cmd/root.go index 655510a..1e96166 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -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().StringVarP(&input.workdir, "directory", "C", ".", "working directory") 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.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)") @@ -156,6 +157,10 @@ func readEnvs(path string, envs map[string]string) bool { //nolint:gocyclo func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []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 == "" { l := log.New() l.SetFormatter(&log.TextFormatter{ @@ -266,6 +271,7 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str Workdir: input.Workdir(), BindWorkdir: input.bindWorkdir, LogOutput: !input.noOutput, + JSONLogger: input.jsonLogger, Env: envs, Secrets: secrets, InsecureSecrets: input.insecureSecrets, diff --git a/pkg/runner/command_test.go b/pkg/runner/command_test.go index bd3c998..bd7bbdc 100644 --- a/pkg/runner/command_test.go +++ b/pkg/runner/command_test.go @@ -157,9 +157,14 @@ func TestAddmaskUsemask(t *testing.T) { a := assert.New(t) + config := &Config{ + Secrets: map[string]string{}, + InsecureSecrets: false, + } + re := captureOutput(t, func() { 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("::add-mask::secret\n") diff --git a/pkg/runner/logger.go b/pkg/runner/logger.go index 0ad2cd1..7949b4b 100644 --- a/pkg/runner/logger.go +++ b/pkg/runner/logger.go @@ -38,14 +38,22 @@ func init() { } // 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() defer mux.Unlock() - formatter := new(stepLogFormatter) - formatter.color = colors[nextColor%len(colors)] - formatter.secrets = secrets - formatter.insecureSecrets = insecureSecrets - formatter.masks = masks + + var formatter logrus.Formatter + if config.JSONLogger { + formatter = &jobLogJSONFormatter{ + 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++ @@ -64,30 +72,39 @@ func WithJobLogger(ctx context.Context, jobName string, secrets map[string]strin return common.WithLogger(ctx, rtn) } -type stepLogFormatter struct { - color int - secrets map[string]string - insecureSecrets bool - masks *[]string +type entryProcessor func(entry *logrus.Entry) *logrus.Entry + +func valueMasker(insecureSecrets bool, secrets map[string]string, masks *[]string) entryProcessor { + return func(entry *logrus.Entry) *logrus.Entry { + 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{} - // Replace any secrets in the entry if insecure-secrets flag is not used - 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, "***") - } - } - } + entry = f.masker(entry) if f.isColored(entry) { f.printColored(b, entry) @@ -99,7 +116,7 @@ func (f *stepLogFormatter) Format(entry *logrus.Entry) ([]byte, error) { 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") 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") 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) if force, ok := os.LookupEnv("CLICOLOR_FORCE"); ok && force != "0" { @@ -147,3 +164,12 @@ func checkIfTerminal(w io.Writer) bool { 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)) +} diff --git a/pkg/runner/runner.go b/pkg/runner/runner.go index 18d499b..d80e0d3 100644 --- a/pkg/runner/runner.go +++ b/pkg/runner/runner.go @@ -31,6 +31,7 @@ type Config struct { ForcePull bool // force pulling of the image, even if already present ForceRebuild bool // force rebuilding local docker image action LogOutput bool // log the output from docker run + JSONLogger bool // use json or text logger Env map[string]string // env for containers Secrets map[string]string // list of secrets InsecureSecrets bool // switch hiding output when printing to terminal @@ -164,7 +165,7 @@ func (runner *runnerImpl) NewPlanExecutor(plan *model.Plan) common.Executor { } 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...))