diff --git a/heartbeat/hbtest/hbtestutil.go b/heartbeat/hbtest/hbtestutil.go index 86c1e4a34d2..0984f76bada 100644 --- a/heartbeat/hbtest/hbtestutil.go +++ b/heartbeat/hbtest/hbtestutil.go @@ -172,6 +172,7 @@ func BaseChecks(ip string, status string, typ string) validator.Validator { } return lookslike.Compose( + HasEventType, lookslike.MustCompile(map[string]interface{}{ "monitor": map[string]interface{}{ "ip": ipCheck, @@ -187,6 +188,12 @@ func BaseChecks(ip string, status string, typ string) validator.Validator { ) } +var HasEventType = lookslike.MustCompile(map[string]interface{}{ + "event": map[string]interface{}{ + "type": isdef.Optional(isdef.IsNonEmptyString), + }, +}) + // SummaryStateChecks validates the "summary" + "state" fields func SummaryStateChecks(up uint16, down uint16) validator.Validator { return lookslike.Compose( diff --git a/heartbeat/monitors/mocks.go b/heartbeat/monitors/mocks.go index 0a7227c9986..18c96522e02 100644 --- a/heartbeat/monitors/mocks.go +++ b/heartbeat/monitors/mocks.go @@ -195,6 +195,7 @@ func baseMockEventMonitorValidator(id string, name string, status string) valida func mockEventMonitorValidator(id string, name string) validator.Validator { return lookslike.Strict(lookslike.Compose( + hbtest.HasEventType, baseMockEventMonitorValidator(id, name, "up"), hbtestllext.MonitorTimespanValidator, hbtest.SummaryStateChecks(1, 0), diff --git a/heartbeat/monitors/wrappers/summarizer/summarizer.go b/heartbeat/monitors/wrappers/summarizer/summarizer.go index 9e330e6623c..3b5d46da3ea 100644 --- a/heartbeat/monitors/wrappers/summarizer/summarizer.go +++ b/heartbeat/monitors/wrappers/summarizer/summarizer.go @@ -60,6 +60,10 @@ type JobSummary struct { RetryGroup string `json:"retry_group"` } +func (js *JobSummary) String() string { + return fmt.Sprintf("", js.Status, js.Attempt, js.MaxAttempts, js.FinalAttempt, js.Up, js.Down, js.RetryGroup) +} + func NewSummarizer(rootJob jobs.Job, sf stdfields.StdMonitorFields, mst *monitorstate.Tracker) *Summarizer { plugins := make([]SumPlugin, 0, 2) if sf.Type == "browser" { @@ -209,10 +213,16 @@ func (ssp *StateStatusPlugin) OnSummary(event *beat.Event) (retry bool) { // dereference the pointer since the pointer is pointed at the next step // after this jsCopy := *ssp.js - eventext.MergeEventFields(event, mapstr.M{ + + fields := mapstr.M{ + "event": mapstr.M{"type": "heartbeat/summary"}, "summary": &jsCopy, "state": ms, - }) + } + if ssp.sf.Type == "browser" { + fields["synthetics"] = mapstr.M{"type": "heartbeat/summary"} + } + eventext.MergeEventFields(event, fields) if retry { // mutate the js into the state for the next attempt diff --git a/heartbeat/monitors/wrappers/summarizer/summarizer_test.go b/heartbeat/monitors/wrappers/summarizer/summarizer_test.go index de86cd7b49a..d9020a29229 100644 --- a/heartbeat/monitors/wrappers/summarizer/summarizer_test.go +++ b/heartbeat/monitors/wrappers/summarizer/summarizer_test.go @@ -51,7 +51,8 @@ func TestSummarizer(t *testing.T) { // The expected states on each event expectedStates string // the attempt number of the given event - expectedAttempts string + expectedAttempts string + expectedSummaries int }{ { "start down, transition to up", @@ -59,6 +60,7 @@ func TestSummarizer(t *testing.T) { "du", "du", "12", + 2, }, { "start up, stay up", @@ -66,6 +68,7 @@ func TestSummarizer(t *testing.T) { "uuuuuuuu", "uuuuuuuu", "11111111", + 8, }, { "start down, stay down", @@ -73,6 +76,7 @@ func TestSummarizer(t *testing.T) { "dddddddd", "dddddddd", "12121212", + 8, }, { "start up - go down with one retry - thenrecover", @@ -80,6 +84,7 @@ func TestSummarizer(t *testing.T) { "udddduuu", "uuddduuu", "11212111", + 8, }, { "start up, transient down, recover", @@ -87,6 +92,7 @@ func TestSummarizer(t *testing.T) { "uuuduuuu", "uuuuuuuu", "11112111", + 8, }, { "start up, multiple transient down, recover", @@ -94,6 +100,7 @@ func TestSummarizer(t *testing.T) { "uuudududu", "uuuuuuuuu", "111121212", + 9, }, { "no retries, single down", @@ -101,6 +108,7 @@ func TestSummarizer(t *testing.T) { "uuuduuuu", "uuuduuuu", "11111111", + 8, }, } @@ -135,6 +143,8 @@ func TestSummarizer(t *testing.T) { rcvdStatuses := "" rcvdStates := "" rcvdAttempts := "" + rcvdEvents := []*beat.Event{} + rcvdSummaries := []*JobSummary{} i := 0 var lastSummary *JobSummary for { @@ -144,6 +154,7 @@ func TestSummarizer(t *testing.T) { wrapped := s.Wrap(job) events, _ := jobs.ExecJobAndConts(t, wrapped) for _, event := range events { + rcvdEvents = append(rcvdEvents, event) eventStatus, _ := event.GetValue("monitor.status") eventStatusStr := eventStatus.(string) rcvdStatuses += eventStatusStr[:1] @@ -155,8 +166,18 @@ func TestSummarizer(t *testing.T) { } summaryIface, _ := event.GetValue("summary") summary := summaryIface.(*JobSummary) + duration, _ := event.GetValue("monitor.duration.us") + + // Ensure that only summaries have a duration + if summary != nil { + rcvdSummaries = append(rcvdSummaries, summary) + require.GreaterOrEqual(t, duration, int64(0)) + } else { + require.Nil(t, duration) + } if summary == nil { + // note missing summaries rcvdAttempts += "!" } else if lastSummary != nil { if summary.Attempt > 1 { @@ -165,6 +186,7 @@ func TestSummarizer(t *testing.T) { require.NotEqual(t, lastSummary.RetryGroup, summary.RetryGroup) } } + rcvdAttempts += fmt.Sprintf("%d", summary.Attempt) lastSummary = summary } @@ -176,6 +198,8 @@ func TestSummarizer(t *testing.T) { require.Equal(t, tt.statusSequence, rcvdStatuses) require.Equal(t, tt.expectedStates, rcvdStates) require.Equal(t, tt.expectedAttempts, rcvdAttempts) + require.Len(t, rcvdEvents, len(tt.statusSequence)) + require.Len(t, rcvdSummaries, tt.expectedSummaries) }) } } diff --git a/heartbeat/monitors/wrappers/wrappers_test.go b/heartbeat/monitors/wrappers/wrappers_test.go index 2f6ad48669c..c45f5cddb13 100644 --- a/heartbeat/monitors/wrappers/wrappers_test.go +++ b/heartbeat/monitors/wrappers/wrappers_test.go @@ -37,7 +37,6 @@ import ( "github.com/elastic/go-lookslike/testslike" "github.com/elastic/go-lookslike/validator" - "github.com/elastic/beats/v7/heartbeat/ecserr" "github.com/elastic/beats/v7/heartbeat/eventext" "github.com/elastic/beats/v7/heartbeat/hbtestllext" "github.com/elastic/beats/v7/heartbeat/monitors/jobs" @@ -598,228 +597,3 @@ func TestTimespan(t *testing.T) { }) } } - -type BrowserMonitor struct { - id string - name string - checkGroup string - durationMs int64 -} - -var inlineMonitorValues = BrowserMonitor{ - id: "inline", - name: "inline", - checkGroup: "inline-check-group", -} - -func makeInlineBrowserJob(t *testing.T, u string) jobs.Job { - parsed, err := url.Parse(u) - require.NoError(t, err) - return func(event *beat.Event) (i []jobs.Job, e error) { - eventext.MergeEventFields(event, mapstr.M{ - "url": URLFields(parsed), - "monitor": mapstr.M{ - "type": "browser", - "status": "up", - }, - }) - return nil, nil - } -} - -func TestInlineBrowserJob(t *testing.T) { - sFields := testBrowserMonFields - sFields.ID = inlineMonitorValues.id - sFields.Name = inlineMonitorValues.name - testCommonWrap(t, testDef{ - "simple", - sFields, - []jobs.Job{makeInlineBrowserJob(t, "http://foo.com")}, - []validator.Validator{ - lookslike.Compose( - urlValidator(t, "http://foo.com"), - lookslike.MustCompile(map[string]interface{}{ - "state": isdef.Optional(hbtestllext.IsMonitorState), - "monitor": map[string]interface{}{ - "type": "browser", - "id": inlineMonitorValues.id, - "name": inlineMonitorValues.name, - "check_group": isdef.IsString, - "duration": mapstr.M{"us": isdef.Optional(isdef.IsDuration)}, - "status": "up", - }, - }), - summarizertesthelper.SummaryValidator(1, 0), - hbtestllext.MonitorTimespanValidator, - ), - }, - nil, - nil, - }) -} - -var projectMonitorValues = BrowserMonitor{ - id: "project-journey_1", - name: "project-Journey 1", - checkGroup: "journey-1-check-group", - durationMs: time.Second.Microseconds(), -} - -func makeProjectBrowserJob(t *testing.T, u string, summary bool, projectErr error, bm BrowserMonitor) jobs.Job { - // TODO: Generate a start, middle, and end event to test summarizing better - parsed, err := url.Parse(u) - require.NoError(t, err) - return func(event *beat.Event) (i []jobs.Job, e error) { - eventext.SetMeta(event, logger.META_STEP_COUNT, 2) - eventext.MergeEventFields(event, mapstr.M{ - "url": URLFields(parsed), - "monitor": mapstr.M{ - "type": "browser", - "id": bm.id, - "name": bm.name, - "status": "up", - "duration": mapstr.M{"us": bm.durationMs}, - }, - }) - if summary { - eventext.MergeEventFields(event, mapstr.M{ - "event": mapstr.M{ - "type": "journey/end", - }, - }) - } - return nil, projectErr - } -} - -var browserLogValidator = func(monId string, expectedDurationUs int64, stepCount int, status string) func(t *testing.T, events []*beat.Event, observed []observer.LoggedEntry) { - return func(t *testing.T, events []*beat.Event, observed []observer.LoggedEntry) { - require.Len(t, observed, 1) - require.Equal(t, "Monitor finished", observed[0].Message) - - expectedMonitor := logger.MonitorRunInfo{ - MonitorID: monId, - Type: "browser", - Duration: expectedDurationUs, - Status: status, - Steps: &stepCount, - } - require.ElementsMatch(t, []zap.Field{ - logp.Any("event", map[string]string{"action": logger.ActionMonitorRun}), - logp.Any("monitor", &expectedMonitor), - }, observed[0].Context) - } -} - -func TestProjectBrowserJob(t *testing.T) { - sFields := testBrowserMonFields - sFields.ID = projectMonitorValues.id - sFields.Name = projectMonitorValues.name - sFields.Origin = "my-origin" - urlStr := "http://foo.com" - urlU, _ := url.Parse(urlStr) - - expectedMonFields := lookslike.Compose( - lookslike.MustCompile(map[string]interface{}{ - "state": isdef.Optional(hbtestllext.IsMonitorState), - "monitor": map[string]interface{}{ - "type": "browser", - "id": projectMonitorValues.id, - "name": projectMonitorValues.name, - "duration": mapstr.M{"us": time.Second.Microseconds()}, - "origin": "my-origin", - "check_group": isdef.IsString, - "timespan": mapstr.M{ - "gte": hbtestllext.IsTime, - "lt": hbtestllext.IsTime, - }, - "status": isdef.IsString, - }, - "url": URLFields(urlU), - }), - ) - - testCommonWrap(t, testDef{ - "simple", // has no summary fields! - sFields, - []jobs.Job{makeProjectBrowserJob(t, urlStr, false, nil, projectMonitorValues)}, - []validator.Validator{ - lookslike.Compose( - summarizertesthelper.SummaryValidator(1, 0), - urlValidator(t, urlStr), - expectedMonFields, - )}, - nil, - nil, - }) - testCommonWrap(t, testDef{ - "with up summary", - sFields, - []jobs.Job{makeProjectBrowserJob(t, urlStr, true, nil, projectMonitorValues)}, - []validator.Validator{ - lookslike.Strict( - lookslike.Compose( - urlValidator(t, urlStr), - expectedMonFields, - summarizertesthelper.SummaryValidator(1, 0), - lookslike.MustCompile(map[string]interface{}{ - "monitor": map[string]interface{}{"status": "up"}, - "event": map[string]interface{}{ - "type": "heartbeat/summary", - }, - }), - ))}, - nil, - browserLogValidator(projectMonitorValues.id, time.Second.Microseconds(), 2, "up"), - }) - testCommonWrap(t, testDef{ - "with down summary", - sFields, - []jobs.Job{makeProjectBrowserJob(t, urlStr, true, fmt.Errorf("testerr"), projectMonitorValues)}, - []validator.Validator{ - lookslike.Strict( - lookslike.Compose( - urlValidator(t, urlStr), - expectedMonFields, - summarizertesthelper.SummaryValidator(0, 1), - lookslike.MustCompile(map[string]interface{}{ - "monitor": map[string]interface{}{"status": "down"}, - "error": map[string]interface{}{ - "type": isdef.IsString, - "message": "testerr", - }, - "event": map[string]interface{}{ - "type": "heartbeat/summary", - }, - }), - ))}, - nil, - browserLogValidator(projectMonitorValues.id, time.Second.Microseconds(), 2, "down"), - }) -} - -func TestECSErrors(t *testing.T) { - // key is test name, value is whether to test a summary event or not - testCases := map[string]bool{ - "on summary event": true, - "on non-summary event": false, - } - - ecse := ecserr.NewBadCmdStatusErr(123, "mycommand") - wrappedECSErr := fmt.Errorf("wrapped: %w", ecse) - expectedECSErr := ecserr.NewECSErr( - ecse.Type, - ecse.Code, - wrappedECSErr.Error(), - ) - - for name, makeSummaryEvent := range testCases { - t.Run(name, func(t *testing.T) { - j := WrapCommon([]jobs.Job{makeProjectBrowserJob(t, "http://example.net", makeSummaryEvent, wrappedECSErr, projectMonitorValues)}, testBrowserMonFields, nil) - event := &beat.Event{} - _, err := j[0](event) - require.NoError(t, err) - require.Equal(t, expectedECSErr, event.Fields["error"]) - }) - } -}