Skip to content

Commit

Permalink
feat(synthetics): addition of a command to run automated tests on mon…
Browse files Browse the repository at this point in the history
…itors (#1508)

Co-authored-by: pranav-new-relic <[email protected]>
Co-authored-by: pranav-new-relic <[email protected]>
  • Loading branch information
3 people authored Aug 30, 2023
1 parent 178f2d5 commit 757304c
Show file tree
Hide file tree
Showing 6 changed files with 311 additions and 16 deletions.
10 changes: 5 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ require (
github.com/jedib0t/go-pretty/v6 v6.4.4
github.com/joshdk/go-junit v0.0.0-20210226021600-6145f504ca0d
github.com/mitchellh/go-homedir v1.1.0
github.com/newrelic/newrelic-client-go/v2 v2.20.0
github.com/newrelic/newrelic-client-go/v2 v2.21.0
github.com/pkg/errors v0.9.1
github.com/shirou/gopsutil/v3 v3.22.12
github.com/sirupsen/logrus v1.9.0
github.com/spf13/cobra v1.6.1
github.com/spf13/cobra v1.7.0
github.com/spf13/pflag v1.0.5
github.com/splitio/go-client/v6 v6.2.1
github.com/stretchr/testify v1.8.1
Expand Down Expand Up @@ -48,21 +48,21 @@ require (
github.com/google/go-querystring v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.1 // indirect
github.com/hashicorp/go-retryablehttp v0.7.0 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/itchyny/timefmt-go v0.1.5 // indirect
github.com/joho/godotenv v1.4.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/mattn/go-zglob v0.0.3 // indirect
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/radovskyb/watcher v1.0.7 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/segmentio/backo-go v1.0.1 // indirect
github.com/splitio/go-split-commons/v4 v4.2.0 // indirect
Expand Down
19 changes: 10 additions & 9 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,8 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/itchyny/gojq v0.12.11 h1:YhLueoHhHiN4mkfM+3AyJV6EPcCxKZsOnYf+aVSwaQw=
github.com/itchyny/gojq v0.12.11/go.mod h1:o3FT8Gkbg/geT4pLI0tF3hvip5F3Y/uskjRz9OYa38g=
github.com/itchyny/timefmt-go v0.1.5 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm6GE=
Expand Down Expand Up @@ -120,8 +120,8 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-zglob v0.0.3 h1:6Ry4EYsScDyt5di4OI6xw1bYhOqfE5S33Z1OPy+d+To=
github.com/mattn/go-zglob v0.0.3/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
Expand All @@ -131,8 +131,8 @@ github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrk
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs=
github.com/newrelic/newrelic-client-go/v2 v2.20.0 h1:87aHvOCnv8MeZL6pm4VptjZKFxatmQq//35onHGBVic=
github.com/newrelic/newrelic-client-go/v2 v2.20.0/go.mod h1:VPWTvEfKvnTZLunAC7fiW33y4e0srznNfN5HJH2cOp8=
github.com/newrelic/newrelic-client-go/v2 v2.21.0 h1:SZ6FEwbLG7nzCJaT402dWPSDvqVegBoCEX7XsAEd3y8=
github.com/newrelic/newrelic-client-go/v2 v2.21.0/go.mod h1:VPWTvEfKvnTZLunAC7fiW33y4e0srznNfN5HJH2cOp8=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
Expand All @@ -157,8 +157,9 @@ github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/radovskyb/watcher v1.0.7 h1:AYePLih6dpmS32vlHfhCeli8127LzkIgwJGcwwe8tUE=
github.com/radovskyb/watcher v1.0.7/go.mod h1:78okwvY5wPdzcb1UYnip1pvrZNIVEIh/Cm+ZuvsUYIg=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.1-0.20210923151022-86f73c517451 h1:d1PiN4RxzIFXCJTvRkvSkKqwtRAl5ZV4lATKtQI0B7I=
github.com/rogpeppe/go-internal v1.8.1-0.20210923151022-86f73c517451/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
Expand All @@ -170,8 +171,8 @@ github.com/shirou/gopsutil/v3 v3.22.12 h1:oG0ns6poeUSxf78JtOsfygNWuEHYYz8hnnNg7P
github.com/shirou/gopsutil/v3 v3.22.12/go.mod h1:Xd7P1kwZcp5VW52+9XsirIKd/BROzbb2wdX3Kqlz9uI=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/splitio/go-client/v6 v6.2.1 h1:EH3xYH7fr2c0I0ZtYvsyn7DjC9ZmoNAFLoKoT3BmQFU=
Expand Down
48 changes: 48 additions & 0 deletions internal/output/text.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,3 +218,51 @@ func (o *Output) newTableWriter() table.Writer {

return t
}

func (o *Output) syntheticNewTableWriter() table.Writer {
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.SetAllowedRowLength(o.terminalWidth)

t.SetStyle(table.Style{
Name: "nr-syn-cli-table",
Box: table.StyleBoxRounded,
Color: table.ColorOptions{
Header: text.Colors{text.Bold},
},
Options: table.Options{
DrawBorder: true,
SeparateColumns: true,
SeparateHeader: true,
},
})

return t
}

// PrintResultTable prints the New Relic Synthetic Atuomated tests
// in a tabular format by default
func PrintResultTable(tableData [][]string) {
o := &Output{terminalWidth: 200}

tw := o.syntheticNewTableWriter()

// Add the header
tw.AppendHeader(table.Row{"Status", "Monitor Name", "Monitor GUID", "isBlocking"})

// Add the rows
for _, row := range tableData {
tw.AppendRow(stringSliceToRow(row))
}

// Render the table
tw.Render()
}

func stringSliceToRow(slice []string) table.Row {
row := make(table.Row, len(slice))
for i, v := range slice {
row[i] = v
}
return row
}
19 changes: 19 additions & 0 deletions internal/synthetics/automated_tests_utilities_yaml.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package synthetics

import "github.com/newrelic/newrelic-client-go/v2/pkg/synthetics"

// a wrapper structure for the input to be sent to syntheticsStartAutomatedTest
type StartAutomatedTestInput struct {
Config synthetics.SyntheticsAutomatedTestConfigInput `json:"config,omitempty"`
Tests []synthetics.SyntheticsAutomatedTestMonitorInput `json:"tests,omitempty"`
}

var globalResultExitCodes = map[synthetics.SyntheticsAutomatedTestStatus]*int{
synthetics.SyntheticsAutomatedTestStatusTypes.FAILED: intPtr(1),
synthetics.SyntheticsAutomatedTestStatusTypes.PASSED: intPtr(0),
synthetics.SyntheticsAutomatedTestStatusTypes.TIMEOUT: intPtr(3),
}

func intPtr(value int) *int {
return &value
}
221 changes: 221 additions & 0 deletions internal/synthetics/command_batch_run.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
package synthetics

import (
"fmt"
"os"
"time"

"github.com/newrelic/newrelic-cli/internal/client"
configAPI "github.com/newrelic/newrelic-cli/internal/config/api"
"github.com/newrelic/newrelic-cli/internal/install/ux"
"github.com/newrelic/newrelic-cli/internal/output"
"github.com/newrelic/newrelic-cli/internal/utils"
"github.com/newrelic/newrelic-client-go/v2/pkg/synthetics"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
)

var (
batchFile string
guid []string
pollingInterval = time.Second * 30
progressIndicator = ux.NewSpinner()
nrdbLatency = time.Second * 5
)

var cmdRun = &cobra.Command{
Use: "run",
Example: "newrelic synthetics run --batchFile filename.yml",
Short: "Start an automated testing job on a batch of synthetic monitors",
Long: `Start an automated testing job on a batch of synthetic monitors
The run command helps start an automated testing job by creating a batch, comprising the specified monitors and their
specifications (such as overrides), and subsequently, keeps fetching the status of the batch at periodic intervals of
time, until the status of the batch, which reflects the consolidated status of all monitors in the batch, is either
success, failure or timed out.
The command may be used with the following flags (the arguments --batchFile and --guid are mutually exclusive).
newrelic synthetics run --batchFile filename.yml
newrelic synthetics run --guid <guid1> --guid <guid2>
`,
Run: func(cmd *cobra.Command, args []string) {
var (
config StartAutomatedTestInput
err error
testsBatchID string
)
accountID := configAPI.GetActiveProfileAccountID()
if batchFile != "" || len(guid) != 0 {
config, err = parseConfiguration()
if err != nil {
log.Fatal(err)
}

testsBatchID = createAutomatedTestBatch(config)
output.Printf("Generated Batch ID: %s", testsBatchID)

// can be ignored if there is no initial tick by the ticker
time.Sleep(nrdbLatency)
getAutomatedTestResults(accountID, testsBatchID)

} else {
utils.LogIfError(cmd.Help())
}

},
}

// Definition of the command
func init() {
cmdRun.Flags().StringVarP(&batchFile, "batchFile", "b", "", "Path to the YAML file comprising GUIDs of monitors and associated configuration")
cmdRun.Flags().StringSliceVarP(&guid, "guid", "g", nil, "List of GUIDs of monitors to include in the batch and run automated tests on")
Command.AddCommand(cmdRun)

// MarkFlagsMutuallyExclusive allows one flag at once be invoked
cmdRun.MarkFlagsMutuallyExclusive("batchFile", "guid")
}

// parseConfiguration helps parse the inputs given to this command, based on the format specified (YAML or command line GUIDs)
func parseConfiguration() (StartAutomatedTestInput, error) {
if batchFile != "" {
return createConfigurationUsingYAML(batchFile)
} else if len(guid) != 0 {
return createConfigurationUsingGUIDs(guid), nil
}
return StartAutomatedTestInput{}, fmt.Errorf("invalid arguments")
}

// createConfigurationUsingYAML unmarshals the specified YAML file into an object that can be used
// to send a create batch request to NerdGraph
func createConfigurationUsingYAML(batchFile string) (StartAutomatedTestInput, error) {
var config StartAutomatedTestInput

content, err := os.ReadFile(batchFile)
if err != nil {
return config, err
}
err = yaml.Unmarshal(content, &config)
if err != nil {
return config, err
}

utils.LogIfFatal(err)
return config, nil
}

// createConfigurationUsingGUIDs obtains GUIDs specified in command line arguments and restructures them into an object
// that can be used to send a create batch request to NerdGraph
func createConfigurationUsingGUIDs(guids []string) StartAutomatedTestInput {
var tests []synthetics.SyntheticsAutomatedTestMonitorInput
for _, id := range guids {
tests = append(tests, synthetics.SyntheticsAutomatedTestMonitorInput{
MonitorGUID: synthetics.EntityGUID(id),
})
}

return StartAutomatedTestInput{
Tests: tests,
}
}

// createAutomatedTestBatch performs an API call to create a batch with the specified configuration and tests
func createAutomatedTestBatch(config StartAutomatedTestInput) string {
if len(config.Tests) == 0 {
log.Fatal("No valid monitors found in the input specified. Please check the input provided.")
}
progressIndicator.Start("Sending a request to create a batch with the specified monitors...")

result, err := client.NRClient.Synthetics.SyntheticsStartAutomatedTest(config.Config, config.Tests)
progressIndicator.Stop()
if err != nil {
utils.LogIfFatal(err)
}

return result.BatchId
}

// getAutomatedTestResults performs an API call at regular intervals of time (when the pollingInterval has elapsed)
// to fetch the consolidated status of the batch, and the results of monitors the batch comprises
func getAutomatedTestResults(accountID int, testsBatchID string) {
// An infinite loop
ticker := time.NewTicker(pollingInterval)
defer ticker.Stop()

for progressIndicator.Start("Fetching the status of tests in the batch...."); true; <-ticker.C {
batchResult, err := client.NRClient.Synthetics.GetAutomatedTestResult(accountID, testsBatchID)
progressIndicator.Stop()

if err != nil {
log.Fatal(err)
}

exitStatus, ok := globalResultExitCodes[(batchResult.Status)]
if !ok {
if batchResult.Status != synthetics.SyntheticsAutomatedTestStatusTypes.IN_PROGRESS {
log.Fatal("Unknown Error")
}
}

renderMonitorTestsSummary(*batchResult, exitStatus)

// Force flush the standard output buffer
os.Stdout.Sync()

// exit, if the status is not IN_PROGRESS
if batchResult.Status != synthetics.SyntheticsAutomatedTestStatusTypes.IN_PROGRESS {
os.Exit(*exitStatus)
}
progressIndicator.Start("Fetching the status of tests in the batch....")
}
}

// renderMonitorTestsSummary reads through the results of monitors fetched, restructures and renders these results accordingly
func renderMonitorTestsSummary(batchResult synthetics.SyntheticsAutomatedTestResult, exitStatus *int) {
fmt.Println("Status: ", batchResult.Status, " ")
summary, tableData := getMonitorTestsSummary(batchResult)
fmt.Printf("Summary: %s\n", summary)
if len(tableData) > 0 {
output.PrintResultTable(tableData)
}

if batchResult.Status != synthetics.SyntheticsAutomatedTestStatusTypes.IN_PROGRESS {
exitStatusMessage := fmt.Sprintf("Exit Status: %d\n", *exitStatus)
fmt.Println(exitStatusMessage)
}
}

// getMonitorTestsSummary reads through the results of monitors fetched and populates them to a table with details
// of each monitor, to print these results to the terminal
func getMonitorTestsSummary(batchResult synthetics.SyntheticsAutomatedTestResult) (string, [][]string) {
results := map[string][]synthetics.SyntheticsAutomatedTestJobResult{}

for _, test := range batchResult.Tests {
if test.Result == "" {
test.Result = synthetics.SyntheticsJobStatusTypes.PENDING
}

results[string(test.Result)] = append(results[string(test.Result)], test)
}

summaryMessage := fmt.Sprintf("%d succeeded; %d failed; %d in progress.",
len(results[string(synthetics.SyntheticsJobStatusTypes.SUCCESS)]),
len(results[string(synthetics.SyntheticsJobStatusTypes.FAILED)]),
len(results[string(synthetics.SyntheticsJobStatusTypes.PENDING)]))

tableData := make([][]string, 0)

for status, tests := range results {
for _, test := range tests {
tableData = append(tableData, []string{
status,
test.MonitorName,
string(test.MonitorGUID),
fmt.Sprintf("%t", test.AutomatedTestMonitorConfig.IsBlocking),
})
}
}

return summaryMessage, tableData
}
Loading

0 comments on commit 757304c

Please sign in to comment.