github.com/smartcontractkit/chainlink-testing-framework/libs@v0.0.0-20240227141906-ec710b4eb1a3/testreporters/reporter_model.go (about)

     1  package testreporters
     2  
     3  import (
     4  	"bufio"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io/fs"
     8  	"os"
     9  	"path/filepath"
    10  	"strings"
    11  	"testing"
    12  	"time"
    13  
    14  	"github.com/rs/zerolog/log"
    15  	"github.com/slack-go/slack"
    16  	"github.com/stretchr/testify/assert"
    17  	"go.uber.org/zap/zapcore"
    18  	"golang.org/x/sync/errgroup"
    19  
    20  	"github.com/smartcontractkit/chainlink-testing-framework/libs/k8s/environment"
    21  )
    22  
    23  type GrafanaURLProvider interface {
    24  	GetGrafanaBaseURL() (string, error)
    25  	GetGrafanaDashboardURL() (string, error)
    26  }
    27  
    28  // TestReporter is a general interface for all test reporters
    29  type TestReporter interface {
    30  	WriteReport(folderLocation string) error
    31  	SendSlackNotification(t *testing.T, slackClient *slack.Client, grafanaUrlProvider GrafanaURLProvider) error
    32  	SetNamespace(namespace string)
    33  }
    34  
    35  const (
    36  	// DefaultArtifactsDir default artifacts dir
    37  	DefaultArtifactsDir string = "logs"
    38  )
    39  
    40  // WriteTeardownLogs attempts to download the logs of all ephemeral test deployments onto the test runner, also writing
    41  // a test report if one is provided. A failing log level also enables you to fail a test based on what level logs the
    42  // Chainlink nodes have thrown during their test.
    43  func WriteTeardownLogs(
    44  	t *testing.T,
    45  	env *environment.Environment,
    46  	optionalTestReporter TestReporter,
    47  	failingLogLevel zapcore.Level, // Chainlink core uses zapcore for logging https://docs.chain.link/chainlink-nodes/v1/configuration#log_level
    48  	grafanaUrlProvider GrafanaURLProvider,
    49  ) error {
    50  	logsPath := filepath.Join(DefaultArtifactsDir, fmt.Sprintf("%s-%s-%d", t.Name(), env.Cfg.Namespace, time.Now().Unix()))
    51  	if err := env.Artifacts.DumpTestResult(logsPath, "chainlink"); err != nil {
    52  		log.Warn().Err(err).Msg("Error trying to collect pod logs")
    53  		return err
    54  	}
    55  	logFiles, err := findAllLogFilesToScan(logsPath)
    56  	if err != nil {
    57  		log.Warn().Err(err).Msg("Error looking for pod logs")
    58  		return err
    59  	}
    60  	verifyLogsGroup := &errgroup.Group{}
    61  	for _, f := range logFiles {
    62  		file := f
    63  		verifyLogsGroup.Go(func() error {
    64  			return verifyLogFile(file, failingLogLevel)
    65  		})
    66  	}
    67  	assert.NoError(t, verifyLogsGroup.Wait(), "Found a concerning log")
    68  
    69  	if t.Failed() || optionalTestReporter != nil {
    70  		if err := SendReport(t, env.Cfg.Namespace, logsPath, optionalTestReporter, grafanaUrlProvider); err != nil {
    71  			log.Warn().Err(err).Msg("Error writing test report")
    72  		}
    73  	}
    74  	return nil
    75  }
    76  
    77  // SendReport writes a test report and sends a Slack notification if the test provides one
    78  func SendReport(t *testing.T, namespace string, logsPath string, optionalTestReporter TestReporter, grafanaUrlProvider GrafanaURLProvider) error {
    79  	if optionalTestReporter != nil {
    80  		log.Info().Msg("Writing Test Report")
    81  		optionalTestReporter.SetNamespace(namespace)
    82  		err := optionalTestReporter.WriteReport(logsPath)
    83  		if err != nil {
    84  			return err
    85  		}
    86  		err = optionalTestReporter.SendSlackNotification(t, nil, grafanaUrlProvider)
    87  		if err != nil {
    88  			return err
    89  		}
    90  	}
    91  	return nil
    92  }
    93  
    94  // findAllLogFilesToScan walks through log files pulled from all pods, and gets all chainlink node logs
    95  func findAllLogFilesToScan(directoryPath string) (logFilesToScan []*os.File, err error) {
    96  	logFilePaths := []string{}
    97  	err = filepath.Walk(directoryPath, func(path string, info fs.FileInfo, err error) error {
    98  		if err != nil {
    99  			return err
   100  		}
   101  		if !info.IsDir() {
   102  			logFilePaths = append(logFilePaths, path)
   103  		}
   104  		return nil
   105  	})
   106  
   107  	for _, filePath := range logFilePaths {
   108  		if strings.Contains(filePath, "node.log") {
   109  			logFileToScan, err := os.Open(filePath)
   110  			if err != nil {
   111  				return nil, err
   112  			}
   113  			logFilesToScan = append(logFilesToScan, logFileToScan)
   114  		}
   115  	}
   116  	return logFilesToScan, err
   117  }
   118  
   119  // allowedLogMessage is a log message that might be thrown by a Chainlink node during a test, but is not a concern
   120  type allowedLogMessage struct {
   121  	message string
   122  	reason  string
   123  	level   zapcore.Level
   124  }
   125  
   126  var allowedLogMessages = []allowedLogMessage{
   127  	{
   128  		message: "No EVM primary nodes available: 0/1 nodes are alive",
   129  		reason:  "Sometimes geth gets unlucky in the start up process and the Chainlink node starts before geth is ready",
   130  		level:   zapcore.DPanicLevel,
   131  	},
   132  }
   133  
   134  // verifyLogFile verifies that a log file
   135  func verifyLogFile(file *os.File, failingLogLevel zapcore.Level) error {
   136  	// nolint
   137  	defer file.Close()
   138  
   139  	var (
   140  		zapLevel zapcore.Level
   141  		err      error
   142  	)
   143  	scanner := bufio.NewScanner(file)
   144  	scanner.Split(bufio.ScanLines)
   145  	for scanner.Scan() {
   146  		jsonLogLine := scanner.Text()
   147  		if !strings.HasPrefix(jsonLogLine, "{") { // don't bother with non-json lines
   148  			if strings.HasPrefix(jsonLogLine, "panic") { // unless it's a panic
   149  				return fmt.Errorf("found panic: %s", jsonLogLine)
   150  			}
   151  			continue
   152  		}
   153  		jsonMapping := map[string]any{}
   154  
   155  		if err = json.Unmarshal([]byte(jsonLogLine), &jsonMapping); err != nil {
   156  			// This error can occur anytime someone uses %+v in a log message, ignoring
   157  			continue
   158  		}
   159  		logLevel, ok := jsonMapping["level"].(string)
   160  		if !ok {
   161  			return fmt.Errorf("found no log level in chainlink log line: %s", jsonLogLine)
   162  		}
   163  
   164  		if logLevel == "crit" { // "crit" is a custom core type they map to DPanic
   165  			zapLevel = zapcore.DPanicLevel
   166  		} else {
   167  			zapLevel, err = zapcore.ParseLevel(logLevel)
   168  			if err != nil {
   169  				return fmt.Errorf("'%s' not a valid zapcore level", logLevel)
   170  			}
   171  		}
   172  
   173  		if zapLevel > failingLogLevel {
   174  			logErr := fmt.Errorf("found log at level '%s', failing any log level higher than %s: %s", logLevel, zapLevel.String(), jsonLogLine)
   175  			logMessage, hasMessage := jsonMapping["msg"]
   176  			if !hasMessage {
   177  				return logErr
   178  			}
   179  			for _, allowedLog := range allowedLogMessages {
   180  				if strings.Contains(logMessage.(string), allowedLog.message) {
   181  					log.Warn().
   182  						Str("Reason", allowedLog.reason).
   183  						Str("Level", allowedLog.level.CapitalString()).
   184  						Str("Msg", logMessage.(string)).
   185  						Msg("Found allowed log message, ignoring")
   186  				}
   187  			}
   188  		}
   189  	}
   190  	return nil
   191  }