github.com/nektos/act@v0.2.63/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  	return func(entry *logrus.Entry) *logrus.Entry {
   148  		if insecureSecrets {
   149  			return entry
   150  		}
   151  
   152  		masks := Masks(entry.Context)
   153  
   154  		for _, v := range secrets {
   155  			if v != "" {
   156  				entry.Message = strings.ReplaceAll(entry.Message, v, "***")
   157  			}
   158  		}
   159  
   160  		for _, v := range *masks {
   161  			if v != "" {
   162  				entry.Message = strings.ReplaceAll(entry.Message, v, "***")
   163  			}
   164  		}
   165  
   166  		return entry
   167  	}
   168  }
   169  
   170  type maskedFormatter struct {
   171  	logrus.Formatter
   172  	masker entryProcessor
   173  }
   174  
   175  func (f *maskedFormatter) Format(entry *logrus.Entry) ([]byte, error) {
   176  	return f.Formatter.Format(f.masker(entry))
   177  }
   178  
   179  type jobLogFormatter struct {
   180  	color          int
   181  	logPrefixJobID bool
   182  }
   183  
   184  func (f *jobLogFormatter) Format(entry *logrus.Entry) ([]byte, error) {
   185  	b := &bytes.Buffer{}
   186  
   187  	if f.isColored(entry) {
   188  		f.printColored(b, entry)
   189  	} else {
   190  		f.print(b, entry)
   191  	}
   192  
   193  	b.WriteByte('\n')
   194  	return b.Bytes(), nil
   195  }
   196  
   197  func (f *jobLogFormatter) printColored(b *bytes.Buffer, entry *logrus.Entry) {
   198  	entry.Message = strings.TrimSuffix(entry.Message, "\n")
   199  
   200  	var job any
   201  	if f.logPrefixJobID {
   202  		job = entry.Data["jobID"]
   203  	} else {
   204  		job = entry.Data["job"]
   205  	}
   206  
   207  	debugFlag := ""
   208  	if entry.Level == logrus.DebugLevel {
   209  		debugFlag = "[DEBUG] "
   210  	}
   211  
   212  	if entry.Data["raw_output"] == true {
   213  		fmt.Fprintf(b, "\x1b[%dm|\x1b[0m %s", f.color, entry.Message)
   214  	} else if entry.Data["dryrun"] == true {
   215  		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)
   216  	} else {
   217  		fmt.Fprintf(b, "\x1b[%dm[%s] \x1b[0m%s%s", f.color, job, debugFlag, entry.Message)
   218  	}
   219  }
   220  
   221  func (f *jobLogFormatter) print(b *bytes.Buffer, entry *logrus.Entry) {
   222  	entry.Message = strings.TrimSuffix(entry.Message, "\n")
   223  
   224  	var job any
   225  	if f.logPrefixJobID {
   226  		job = entry.Data["jobID"]
   227  	} else {
   228  		job = entry.Data["job"]
   229  	}
   230  
   231  	debugFlag := ""
   232  	if entry.Level == logrus.DebugLevel {
   233  		debugFlag = "[DEBUG] "
   234  	}
   235  
   236  	if entry.Data["raw_output"] == true {
   237  		fmt.Fprintf(b, "[%s]   | %s", job, entry.Message)
   238  	} else if entry.Data["dryrun"] == true {
   239  		fmt.Fprintf(b, "*DRYRUN* [%s] %s%s", job, debugFlag, entry.Message)
   240  	} else {
   241  		fmt.Fprintf(b, "[%s] %s%s", job, debugFlag, entry.Message)
   242  	}
   243  }
   244  
   245  func (f *jobLogFormatter) isColored(entry *logrus.Entry) bool {
   246  	isColored := checkIfTerminal(entry.Logger.Out)
   247  
   248  	if force, ok := os.LookupEnv("CLICOLOR_FORCE"); ok && force != "0" {
   249  		isColored = true
   250  	} else if ok && force == "0" {
   251  		isColored = false
   252  	} else if os.Getenv("CLICOLOR") == "0" {
   253  		isColored = false
   254  	}
   255  
   256  	return isColored
   257  }
   258  
   259  func checkIfTerminal(w io.Writer) bool {
   260  	switch v := w.(type) {
   261  	case *os.File:
   262  		return term.IsTerminal(int(v.Fd()))
   263  	default:
   264  		return false
   265  	}
   266  }