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 }