github.com/nektos/act@v0.2.83/pkg/runner/logger.go (about)

     1  package runner
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"strings"
    10  	"sync"
    11  
    12  	"github.com/nektos/act/pkg/common"
    13  
    14  	"github.com/sirupsen/logrus"
    15  	"golang.org/x/term"
    16  )
    17  
    18  const (
    19  	// nocolor = 0
    20  	red     = 31
    21  	green   = 32
    22  	yellow  = 33
    23  	blue    = 34
    24  	magenta = 35
    25  	cyan    = 36
    26  	gray    = 37
    27  )
    28  
    29  var colors []int
    30  var nextColor int
    31  var mux sync.Mutex
    32  
    33  func init() {
    34  	nextColor = 0
    35  	colors = []int{
    36  		blue, yellow, green, magenta, red, gray, cyan,
    37  	}
    38  }
    39  
    40  type masksContextKey string
    41  
    42  const masksContextKeyVal = masksContextKey("logrus.FieldLogger")
    43  
    44  // Logger returns the appropriate logger for current context
    45  func Masks(ctx context.Context) *[]string {
    46  	val := ctx.Value(masksContextKeyVal)
    47  	if val != nil {
    48  		if masks, ok := val.(*[]string); ok {
    49  			return masks
    50  		}
    51  	}
    52  	return &[]string{}
    53  }
    54  
    55  // WithMasks adds a value to the context for the logger
    56  func WithMasks(ctx context.Context, masks *[]string) context.Context {
    57  	return context.WithValue(ctx, masksContextKeyVal, masks)
    58  }
    59  
    60  type JobLoggerFactory interface {
    61  	WithJobLogger() *logrus.Logger
    62  }
    63  
    64  type jobLoggerFactoryContextKey string
    65  
    66  var jobLoggerFactoryContextKeyVal = (jobLoggerFactoryContextKey)("jobloggerkey")
    67  
    68  func WithJobLoggerFactory(ctx context.Context, factory JobLoggerFactory) context.Context {
    69  	return context.WithValue(ctx, jobLoggerFactoryContextKeyVal, factory)
    70  }
    71  
    72  // WithJobLogger attaches a new logger to context that is aware of steps
    73  func WithJobLogger(ctx context.Context, jobID string, jobName string, config *Config, masks *[]string, matrix map[string]interface{}) context.Context {
    74  	ctx = WithMasks(ctx, masks)
    75  
    76  	var logger *logrus.Logger
    77  	if jobLoggerFactory, ok := ctx.Value(jobLoggerFactoryContextKeyVal).(JobLoggerFactory); ok && jobLoggerFactory != nil {
    78  		logger = jobLoggerFactory.WithJobLogger()
    79  	} else {
    80  		var formatter logrus.Formatter
    81  		if config.JSONLogger {
    82  			formatter = &logrus.JSONFormatter{}
    83  		} else {
    84  			mux.Lock()
    85  			defer mux.Unlock()
    86  			nextColor++
    87  			formatter = &jobLogFormatter{
    88  				color:          colors[nextColor%len(colors)],
    89  				logPrefixJobID: config.LogPrefixJobID,
    90  			}
    91  		}
    92  
    93  		logger = logrus.New()
    94  		logger.SetOutput(os.Stdout)
    95  		logger.SetLevel(logrus.GetLevel())
    96  		logger.SetFormatter(formatter)
    97  	}
    98  
    99  	logger.SetFormatter(&maskedFormatter{
   100  		Formatter: logger.Formatter,
   101  		masker:    valueMasker(config.InsecureSecrets, config.Secrets),
   102  	})
   103  	rtn := logger.WithFields(logrus.Fields{
   104  		"job":    jobName,
   105  		"jobID":  jobID,
   106  		"dryrun": common.Dryrun(ctx),
   107  		"matrix": matrix,
   108  	}).WithContext(ctx)
   109  
   110  	return common.WithLogger(ctx, rtn)
   111  }
   112  
   113  func WithCompositeLogger(ctx context.Context, masks *[]string) context.Context {
   114  	ctx = WithMasks(ctx, masks)
   115  	return common.WithLogger(ctx, common.Logger(ctx).WithFields(logrus.Fields{}).WithContext(ctx))
   116  }
   117  
   118  func WithCompositeStepLogger(ctx context.Context, stepID string) context.Context {
   119  	val := common.Logger(ctx)
   120  	stepIDs := make([]string, 0)
   121  
   122  	if logger, ok := val.(*logrus.Entry); ok {
   123  		if oldStepIDs, ok := logger.Data["stepID"].([]string); ok {
   124  			stepIDs = append(stepIDs, oldStepIDs...)
   125  		}
   126  	}
   127  
   128  	stepIDs = append(stepIDs, stepID)
   129  
   130  	return common.WithLogger(ctx, common.Logger(ctx).WithFields(logrus.Fields{
   131  		"stepID": stepIDs,
   132  	}).WithContext(ctx))
   133  }
   134  
   135  func withStepLogger(ctx context.Context, stepID string, stepName string, stageName string) context.Context {
   136  	rtn := common.Logger(ctx).WithFields(logrus.Fields{
   137  		"step":   stepName,
   138  		"stepID": []string{stepID},
   139  		"stage":  stageName,
   140  	})
   141  	return common.WithLogger(ctx, rtn)
   142  }
   143  
   144  type entryProcessor func(entry *logrus.Entry) *logrus.Entry
   145  
   146  func valueMasker(insecureSecrets bool, secrets map[string]string) entryProcessor {
   147  	ssecrets := []string{}
   148  	for _, v := range secrets {
   149  		ssecrets = append(ssecrets, v)
   150  	}
   151  	return func(entry *logrus.Entry) *logrus.Entry {
   152  		if insecureSecrets {
   153  			return entry
   154  		}
   155  
   156  		masks := Masks(entry.Context)
   157  
   158  		for _, v := range ssecrets {
   159  			if v != "" {
   160  				entry.Message = strings.ReplaceAll(entry.Message, v, "***")
   161  			}
   162  		}
   163  
   164  		for _, v := range *masks {
   165  			if v != "" {
   166  				entry.Message = strings.ReplaceAll(entry.Message, v, "***")
   167  			}
   168  		}
   169  
   170  		return entry
   171  	}
   172  }
   173  
   174  type maskedFormatter struct {
   175  	logrus.Formatter
   176  	masker entryProcessor
   177  }
   178  
   179  func (f *maskedFormatter) Format(entry *logrus.Entry) ([]byte, error) {
   180  	return f.Formatter.Format(f.masker(entry))
   181  }
   182  
   183  type jobLogFormatter struct {
   184  	color          int
   185  	logPrefixJobID bool
   186  }
   187  
   188  func (f *jobLogFormatter) Format(entry *logrus.Entry) ([]byte, error) {
   189  	b := &bytes.Buffer{}
   190  
   191  	if f.isColored(entry) {
   192  		f.printColored(b, entry)
   193  	} else {
   194  		f.print(b, entry)
   195  	}
   196  
   197  	b.WriteByte('\n')
   198  	return b.Bytes(), nil
   199  }
   200  
   201  func (f *jobLogFormatter) printColored(b *bytes.Buffer, entry *logrus.Entry) {
   202  	entry.Message = strings.TrimSuffix(entry.Message, "\n")
   203  
   204  	var job any
   205  	if f.logPrefixJobID {
   206  		job = entry.Data["jobID"]
   207  	} else {
   208  		job = entry.Data["job"]
   209  	}
   210  
   211  	debugFlag := ""
   212  	if entry.Level == logrus.DebugLevel {
   213  		debugFlag = "[DEBUG] "
   214  	}
   215  
   216  	if entry.Data["raw_output"] == true {
   217  		fmt.Fprintf(b, "\x1b[%dm|\x1b[0m %s", f.color, entry.Message)
   218  	} else if entry.Data["dryrun"] == true {
   219  		fmt.Fprintf(b, "\x1b[1m\x1b[%dm\x1b[7m*DRYRUN*\x1b[0m \x1b[%dm[%s] \x1b[0m%s%s", gray, f.color, job, debugFlag, entry.Message)
   220  	} else {
   221  		fmt.Fprintf(b, "\x1b[%dm[%s] \x1b[0m%s%s", f.color, job, debugFlag, entry.Message)
   222  	}
   223  }
   224  
   225  func (f *jobLogFormatter) print(b *bytes.Buffer, entry *logrus.Entry) {
   226  	entry.Message = strings.TrimSuffix(entry.Message, "\n")
   227  
   228  	var job any
   229  	if f.logPrefixJobID {
   230  		job = entry.Data["jobID"]
   231  	} else {
   232  		job = entry.Data["job"]
   233  	}
   234  
   235  	debugFlag := ""
   236  	if entry.Level == logrus.DebugLevel {
   237  		debugFlag = "[DEBUG] "
   238  	}
   239  
   240  	if entry.Data["raw_output"] == true {
   241  		fmt.Fprintf(b, "[%s]   | %s", job, entry.Message)
   242  	} else if entry.Data["dryrun"] == true {
   243  		fmt.Fprintf(b, "*DRYRUN* [%s] %s%s", job, debugFlag, entry.Message)
   244  	} else {
   245  		fmt.Fprintf(b, "[%s] %s%s", job, debugFlag, entry.Message)
   246  	}
   247  }
   248  
   249  func (f *jobLogFormatter) isColored(entry *logrus.Entry) bool {
   250  	isColored := checkIfTerminal(entry.Logger.Out)
   251  
   252  	if force, ok := os.LookupEnv("CLICOLOR_FORCE"); ok && force != "0" {
   253  		isColored = true
   254  	} else if ok && force == "0" {
   255  		isColored = false
   256  	} else if os.Getenv("CLICOLOR") == "0" {
   257  		isColored = false
   258  	}
   259  
   260  	return isColored
   261  }
   262  
   263  func checkIfTerminal(w io.Writer) bool {
   264  	switch v := w.(type) {
   265  	case *os.File:
   266  		return term.IsTerminal(int(v.Fd()))
   267  	default:
   268  		return false
   269  	}
   270  }