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  }