github.com/darmach/terratest@v0.34.8-0.20210517103231-80931f95e3ff/cmd/terratest_log_parser/main.go (about) 1 // A CLI command to parse parallel terratest output to produce test summaries and break out interleaved test output. 2 // 3 // This command will take as input a terratest log output from either stdin (through a pipe) or from a file, and output 4 // to a directory the following files: 5 // outputDir 6 // |-> TEST_NAME.log 7 // |-> summary.log 8 // |-> report.xml 9 // where: 10 // - `TEST_NAME.log` is a log for each test run that only includes the relevant logs for that test. 11 // - `summary.log` is a summary of all the tests in the suite, including PASS/FAIL information. 12 // - `report.xml` is the test summary in junit XML format to be consumed by a CI engine. 13 // 14 // Certain tradeoffs were made in the decision to implement this functionality as a separate parsing command, as opposed 15 // to being built into the logger module as part of `Logf`. Specifically, this implementation avoids the difficulties of 16 // hooking into go's testing framework to be able to extract the summary logs, at the expense of a more complicated 17 // implementation in handling various corner cases due to logging flexibility. Here are the list of pros and cons of 18 // this approach that were considered: 19 // 20 // Pros: 21 // - Robust to unexpected failures in testing code, like `ctrl+c`, panics, OOM kills and the like since the parser is 22 // not tied to the testing process. This approach is less likely to miss these entries, and can be surfaced to the 23 // summary view for easy viewing in CI engines (no need to scroll), like the panic example. 24 // - Can combine `go test` output (e.g `--- PASS` entries) with the log entries for the test in a single log file. 25 // - Can extract out the summary view (those are all `go test` log entries). 26 // - Can build `junit.xml` report that CI engines can use for test insights. 27 // 28 // Cons: 29 // - Complicated implementation that is potentially brittle. E.g if someone decides to change the logging format then 30 // this will all break. If we hook during the test, then the implementation is easier because those logs are all emitted 31 // at certain points in code, the information of which is lost in the final log and have to parse out. 32 // - Have to store all the logs twice (the full interleaved version, and the broken out version) because the parsing 33 // depends on logs being available. (NOTE: this is avoidable with a pipe). 34 35 package main 36 37 import ( 38 "fmt" 39 "os" 40 "path/filepath" 41 42 "github.com/gruntwork-io/go-commons/entrypoint" 43 "github.com/gruntwork-io/go-commons/errors" 44 "github.com/gruntwork-io/go-commons/logging" 45 "github.com/gruntwork-io/terratest/modules/logger/parser" 46 "github.com/sirupsen/logrus" 47 "github.com/urfave/cli" 48 ) 49 50 var logger = logging.GetLogger("terratest_log_parser") 51 52 const CUSTOM_USAGE_TEXT = `Usage: terratest_log_parser [--help] [--log-level=info] [--testlog=LOG_INPUT] [--outputdir=OUTPUT_DIR] 53 54 A tool for parsing parallel terratest output to produce a test summary and to break out the interleaved logs by test for better debuggability. 55 56 Options: 57 --log-level LEVEL Set the log level to LEVEL. Must be one of: [panic fatal error warning info debug] 58 (default: "info") 59 --testlog value Path to file containing test log. If unset will use stdin. 60 --outputdir value Path to directory to output test output to. If unset will use the current directory. 61 --help, -h show help 62 ` 63 64 func run(cliContext *cli.Context) error { 65 filename := cliContext.String("testlog") 66 outputDir := cliContext.String("outputdir") 67 logLevel := cliContext.String("log-level") 68 level, err := logrus.ParseLevel(logLevel) 69 if err != nil { 70 return errors.WithStackTrace(err) 71 } 72 logger.SetLevel(level) 73 74 var file *os.File 75 if filename != "" { 76 logger.Infof("reading from file") 77 file, err = os.Open(filename) 78 if err != nil { 79 logger.Fatalf("Error opening file: %s", err) 80 } 81 } else { 82 logger.Infof("reading from stdin") 83 file = os.Stdin 84 } 85 defer file.Close() 86 87 outputDir, err = filepath.Abs(outputDir) 88 if err != nil { 89 logger.Fatalf("Error extracting absolute path of output directory: %s", err) 90 } 91 92 parser.SpawnParsers(logger, file, outputDir) 93 return nil 94 } 95 96 func main() { 97 app := entrypoint.NewApp() 98 cli.AppHelpTemplate = CUSTOM_USAGE_TEXT 99 entrypoint.HelpTextLineWidth = 120 100 101 app.Name = "terratest_log_parser" 102 app.Author = "Gruntwork <www.gruntwork.io>" 103 app.Description = `A tool for parsing parallel terratest output to produce a test summary and to break out the interleaved logs by test for better debuggability.` 104 app.Action = run 105 106 currentDir, err := os.Getwd() 107 if err != nil { 108 logger.Fatalf("Error finding current directory: %s", err) 109 } 110 defaultOutputDir := filepath.Join(currentDir, "out") 111 112 logInputFlag := cli.StringFlag{ 113 Name: "testlog, l", 114 Value: "", 115 Usage: "Path to file containing test log. If unset will use stdin.", 116 } 117 outputDirFlag := cli.StringFlag{ 118 Name: "outputdir, o", 119 Value: defaultOutputDir, 120 Usage: "Path to directory to output test output to. If unset will use the current directory.", 121 } 122 logLevelFlag := cli.StringFlag{ 123 Name: "log-level", 124 Value: logrus.InfoLevel.String(), 125 Usage: fmt.Sprintf("Set the log level to `LEVEL`. Must be one of: %v", logrus.AllLevels), 126 } 127 app.Flags = []cli.Flag{ 128 logLevelFlag, 129 logInputFlag, 130 outputDirFlag, 131 } 132 133 entrypoint.RunApp(app) 134 }