act/pkg/runner/logger.go
Robert Kowalski 5f5b8959d6
fix: add parent step id in composite action step id ()
when running nested composite actions, step ids were repeating
leading to errors in parsing the output. this patch adds the
parent step id to ste stepID field.

Co-authored-by: Björn Brauer <bjoern.brauer@new-work.se>
Co-authored-by: Markus Wolf <markus.wolf@new-work.se>

Co-authored-by: Björn Brauer <bjoern.brauer@new-work.se>
Co-authored-by: Markus Wolf <markus.wolf@new-work.se>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2022-07-27 19:56:41 +00:00

235 lines
5.5 KiB
Go

package runner
import (
"bytes"
"context"
"fmt"
"io"
"os"
"strings"
"sync"
"github.com/nektos/act/pkg/common"
"github.com/sirupsen/logrus"
"golang.org/x/term"
)
const (
// nocolor = 0
red = 31
green = 32
yellow = 33
blue = 34
magenta = 35
cyan = 36
gray = 37
)
var colors []int
var nextColor int
var mux sync.Mutex
func init() {
nextColor = 0
colors = []int{
blue, yellow, green, magenta, red, gray, cyan,
}
}
type masksContextKey string
const masksContextKeyVal = masksContextKey("logrus.FieldLogger")
// Logger returns the appropriate logger for current context
func Masks(ctx context.Context) *[]string {
val := ctx.Value(masksContextKeyVal)
if val != nil {
if masks, ok := val.(*[]string); ok {
return masks
}
}
return &[]string{}
}
// WithLogger adds a value to the context for the logger
func WithMasks(ctx context.Context, masks *[]string) context.Context {
return context.WithValue(ctx, masksContextKeyVal, masks)
}
// WithJobLogger attaches a new logger to context that is aware of steps
func WithJobLogger(ctx context.Context, jobID string, jobName string, config *Config, masks *[]string) context.Context {
mux.Lock()
defer mux.Unlock()
var formatter logrus.Formatter
if config.JSONLogger {
formatter = &jobLogJSONFormatter{
formatter: &logrus.JSONFormatter{},
masker: valueMasker(config.InsecureSecrets, config.Secrets),
}
} else {
formatter = &jobLogFormatter{
color: colors[nextColor%len(colors)],
masker: valueMasker(config.InsecureSecrets, config.Secrets),
}
}
nextColor++
ctx = WithMasks(ctx, masks)
logger := logrus.New()
logger.SetFormatter(formatter)
logger.SetOutput(os.Stdout)
logger.SetLevel(logrus.GetLevel())
rtn := logger.WithFields(logrus.Fields{
"job": jobName,
"jobID": jobID,
"dryrun": common.Dryrun(ctx),
}).WithContext(ctx)
return common.WithLogger(ctx, rtn)
}
func WithCompositeLogger(ctx context.Context, masks *[]string) context.Context {
ctx = WithMasks(ctx, masks)
return common.WithLogger(ctx, common.Logger(ctx).WithFields(logrus.Fields{}).WithContext(ctx))
}
func WithCompositeStepLogger(ctx context.Context, stepID string) context.Context {
val := common.Logger(ctx)
stepIDs := make([]string, 0)
if logger, ok := val.(*logrus.Entry); ok {
if oldStepIDs, ok := logger.Data["stepID"].([]string); ok {
stepIDs = append(stepIDs, oldStepIDs...)
}
}
stepIDs = append(stepIDs, stepID)
return common.WithLogger(ctx, common.Logger(ctx).WithFields(logrus.Fields{
"stepID": stepIDs,
}).WithContext(ctx))
}
func withStepLogger(ctx context.Context, stepID string, stepName string, stageName string) context.Context {
rtn := common.Logger(ctx).WithFields(logrus.Fields{
"step": stepName,
"stepID": []string{stepID},
"stage": stageName,
})
return common.WithLogger(ctx, rtn)
}
type entryProcessor func(entry *logrus.Entry) *logrus.Entry
func valueMasker(insecureSecrets bool, secrets map[string]string) entryProcessor {
return func(entry *logrus.Entry) *logrus.Entry {
if insecureSecrets {
return entry
}
masks := Masks(entry.Context)
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
}
}
type jobLogFormatter struct {
color int
masker entryProcessor
}
func (f *jobLogFormatter) Format(entry *logrus.Entry) ([]byte, error) {
b := &bytes.Buffer{}
entry = f.masker(entry)
if f.isColored(entry) {
f.printColored(b, entry)
} else {
f.print(b, entry)
}
b.WriteByte('\n')
return b.Bytes(), nil
}
func (f *jobLogFormatter) printColored(b *bytes.Buffer, entry *logrus.Entry) {
entry.Message = strings.TrimSuffix(entry.Message, "\n")
jobName := entry.Data["job"]
debugFlag := ""
if entry.Level == logrus.DebugLevel {
debugFlag = "[DEBUG] "
}
if entry.Data["raw_output"] == true {
fmt.Fprintf(b, "\x1b[%dm|\x1b[0m %s", f.color, entry.Message)
} else if entry.Data["dryrun"] == true {
fmt.Fprintf(b, "\x1b[1m\x1b[%dm\x1b[7m*DRYRUN*\x1b[0m \x1b[%dm[%s] \x1b[0m%s%s", gray, f.color, jobName, debugFlag, entry.Message)
} else {
fmt.Fprintf(b, "\x1b[%dm[%s] \x1b[0m%s%s", f.color, jobName, debugFlag, entry.Message)
}
}
func (f *jobLogFormatter) print(b *bytes.Buffer, entry *logrus.Entry) {
entry.Message = strings.TrimSuffix(entry.Message, "\n")
jobName := entry.Data["job"]
debugFlag := ""
if entry.Level == logrus.DebugLevel {
debugFlag = "[DEBUG] "
}
if entry.Data["raw_output"] == true {
fmt.Fprintf(b, "[%s] | %s", jobName, entry.Message)
} else if entry.Data["dryrun"] == true {
fmt.Fprintf(b, "*DRYRUN* [%s] %s%s", jobName, debugFlag, entry.Message)
} else {
fmt.Fprintf(b, "[%s] %s%s", jobName, debugFlag, entry.Message)
}
}
func (f *jobLogFormatter) isColored(entry *logrus.Entry) bool {
isColored := checkIfTerminal(entry.Logger.Out)
if force, ok := os.LookupEnv("CLICOLOR_FORCE"); ok && force != "0" {
isColored = true
} else if ok && force == "0" {
isColored = false
} else if os.Getenv("CLICOLOR") == "0" {
isColored = false
}
return isColored
}
func checkIfTerminal(w io.Writer) bool {
switch v := w.(type) {
case *os.File:
return term.IsTerminal(int(v.Fd()))
default:
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))
}