Skip to content

Commit

Permalink
feat(crons): initial cron support
Browse files Browse the repository at this point in the history
  • Loading branch information
aldy505 committed Jul 4, 2023
1 parent 2563bd8 commit 81e5401
Show file tree
Hide file tree
Showing 9 changed files with 308 additions and 0 deletions.
110 changes: 110 additions & 0 deletions check_in.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package sentry

type CheckInStatus string

const (
CheckInStatusInProgress CheckInStatus = "in_progress"
CheckInStatusOK CheckInStatus = "ok"
CheckInStatusError CheckInStatus = "error"
)

type checkInScheduleType string

const (
checkInScheduleTypeCrontab checkInScheduleType = "crontab"
checkInScheduleTypeInterval checkInScheduleType = "interval"
)

type MonitorSchedule interface {
scheduleType() checkInScheduleType
}

type crontabSchedule struct {
Type string `json:"type"`
Value string `json:"value"`
}

func (c crontabSchedule) scheduleType() checkInScheduleType {
return checkInScheduleTypeCrontab
}

// CrontabSchedule defines the MonitorSchedule with a cron format.
// Example: "8 * * * *".
func CrontabSchedule(scheduleString string) MonitorSchedule {
return crontabSchedule{
Type: string(checkInScheduleTypeCrontab),
Value: scheduleString,
}
}

type intervalSchedule struct {
Type string `json:"type"`
Value int64 `json:"value"`
Unit string `json:"unit"`
}

func (i intervalSchedule) scheduleType() checkInScheduleType {
return checkInScheduleTypeInterval
}

type MonitorScheduleUnit string

const (
MonitorScheduleUnitMinute MonitorScheduleUnit = "minute"
MonitorScheduleUnitHour MonitorScheduleUnit = "hour"
MonitorScheduleUnitDay MonitorScheduleUnit = "day"
MonitorScheduleUnitWeek MonitorScheduleUnit = "week"
MonitorScheduleUnitMonth MonitorScheduleUnit = "month"
MonitorScheduleUnitYear MonitorScheduleUnit = "year"
)

// IntervalSchedule defines the MonitorSchedule with an interval format.
//
// Example:
//
// IntervalSchedule(1, sentry.MonitorScheduleUnitDay)
func IntervalSchedule(value int64, unit MonitorScheduleUnit) MonitorSchedule {
return intervalSchedule{
Type: string(checkInScheduleTypeInterval),
Value: value,
Unit: string(unit),
}
}

type MonitorConfig struct { //nolint: maligned // prefer readability over optimal memory layout
Schedule MonitorSchedule `json:"schedule,omitempty"`
// The allowed allowed margin of minutes after the expected check-in time that
// the monitor will not be considered missed for.
CheckInMargin int64 `json:"check_in_margin,omitempty"`
// The allowed allowed duration in minutes that the monitor may be `in_progress`
// for before being considered failed due to timeout.
MaxRuntime int64 `json:"max_runtime,omitempty"`
// A tz database string representing the timezone which the monitor's execution schedule is in.
// See: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
Timezone string `json:"timezone,omitempty"`
}

type CheckIn struct { //nolint: maligned // prefer readability over optimal memory layout
// The distinct slug of the monitor.
MonitorSlug string `json:"monitor_slug"`
// The status of the check-in.
Status CheckInStatus `json:"status"`
// The duration of the check-in in seconds. Will only take effect if the status is ok or error.
Duration int64 `json:"duration,omitempty"`
}

// serializedCheckIn is used by checkInMarshalJSON method on Event struct.
// See https://develop.sentry.dev/sdk/check-ins/
type serializedCheckIn struct { //nolint: maligned
// Check-In ID (unique and client generated).
CheckInID string `json:"check_in_id"`
// The distinct slug of the monitor.
MonitorSlug string `json:"monitor_slug"`
// The status of the check-in.
Status CheckInStatus `json:"status"`
// The duration of the check-in in seconds. Will only take effect if the status is ok or error.
Duration int64 `json:"duration,omitempty"`
Release string `json:"release,omitempty"`
Environment string `json:"environment,omitempty"`
MonitorConfig *MonitorConfig `json:"monitor_config,omitempty"`
}
16 changes: 16 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,12 @@ func (client *Client) CaptureException(exception error, hint *EventHint, scope E
return client.CaptureEvent(event, hint, scope)
}

// CaptureCheckIn captures a check in.
func (client *Client) CaptureCheckIn(checkIn *CheckIn, monitorConfig *MonitorConfig, scope EventModifier) *EventID {
event := client.EventFromCheckIn(checkIn, monitorConfig)
return client.processEvent(event, nil, scope)
}

// CaptureEvent captures an event on the currently active client if any.
//
// The event must already be assembled. Typically code would instead use
Expand Down Expand Up @@ -524,6 +530,16 @@ func (client *Client) EventFromException(exception error, level Level) *Event {
return event
}

// EventFromCheckIn creates a new Sentry event from the given `check_in` instance.
func (client *Client) EventFromCheckIn(checkIn *CheckIn, monitorConfig *MonitorConfig) *Event {
event := NewEvent()
event.CheckInID = uuid()
event.CheckIn = checkIn
event.MonitorConfig = monitorConfig

return event
}

// reverse reverses the slice a in place.
func reverse(a []Exception) {
for i := len(a)/2 - 1; i >= 0; i-- {
Expand Down
19 changes: 19 additions & 0 deletions hub.go
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,25 @@ func (hub *Hub) CaptureException(exception error) *EventID {
return eventID
}

// CaptureCheckIn calls the method of a same nname on currently bound Client instance
// passing it a top-level Scope.
// Returns EventID if successfully, or nil if there's no Client available.
func (hub *Hub) CaptureCheckIn(checkIn *CheckIn, monitorConfig *MonitorConfig) *EventID {
client, scope := hub.Client(), hub.Scope()
if client == nil {
return nil
}

eventID := client.CaptureCheckIn(checkIn, monitorConfig, scope)
if eventID != nil {
hub.mu.Lock()
hub.lastEventID = *eventID
hub.mu.Unlock()
}

return eventID
}

// AddBreadcrumb records a new breadcrumb.
//
// The total number of breadcrumbs that can be recorded are limited by the
Expand Down
33 changes: 33 additions & 0 deletions interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ const eventType = "event"

const profileType = "profile"

// checkInType is the type of a check in event.
const checkInType = "check_in"

// Level marks the severity of the event.
type Level string

Expand Down Expand Up @@ -315,6 +318,12 @@ type Event struct {
Spans []*Span `json:"spans,omitempty"`
TransactionInfo *TransactionInfo `json:"transaction_info,omitempty"`

// The fields below are only relevant for crons/check ins

CheckInID string `json:"check_in_id,omitempty"`
CheckIn *CheckIn `json:"check_in,omitempty"`
MonitorConfig *MonitorConfig `json:"monitor_config,omitempty"`

// The fields below are not part of the final JSON payload.

sdkMetaData SDKMetaData
Expand Down Expand Up @@ -375,6 +384,8 @@ func (e *Event) MarshalJSON() ([]byte, error) {
// and a few type tricks.
if e.Type == transactionType {
return e.transactionMarshalJSON()
} else if e.Type == checkInType {
return e.checkInMarshalJSON()
}
return e.defaultMarshalJSON()
}
Expand Down Expand Up @@ -449,6 +460,28 @@ func (e *Event) transactionMarshalJSON() ([]byte, error) {
return json.Marshal(x)
}

func (e *Event) checkInMarshalJSON() ([]byte, error) {
checkIn := serializedCheckIn{
CheckInID: e.CheckInID,
MonitorSlug: e.CheckIn.MonitorSlug,
Status: e.CheckIn.Status,
Duration: e.CheckIn.Duration,
Release: e.Release,
Environment: e.Environment,
MonitorConfig: nil,
}

if e.MonitorConfig != nil {
checkIn.MonitorConfig = &MonitorConfig{}
checkIn.MonitorConfig.Schedule = e.MonitorConfig.Schedule
checkIn.MonitorConfig.CheckInMargin = e.MonitorConfig.CheckInMargin
checkIn.MonitorConfig.MaxRuntime = e.MonitorConfig.MaxRuntime
checkIn.MonitorConfig.Timezone = e.MonitorConfig.Timezone
}

return json.Marshal(checkIn)
}

// NewEvent creates a new Event.
func NewEvent() *Event {
event := Event{
Expand Down
81 changes: 81 additions & 0 deletions marshal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,87 @@ func TestTransactionEventMarshalJSON(t *testing.T) {
}
}

func TestCheckInEventMarshalJSON(t *testing.T) {
tests := []*Event{
{
Release: "1.0.0",
Environment: "dev",
Type: checkInType,
CheckInID: uuid(),
CheckIn: &CheckIn{
MonitorSlug: "my-monitor",
Status: "ok",
Duration: 10,
},
MonitorConfig: nil,
},
{
Release: "1.0.0",
Environment: "dev",
Type: checkInType,
CheckInID: uuid(),
CheckIn: &CheckIn{
MonitorSlug: "my-monitor",
Status: "ok",
Duration: 10,
},
MonitorConfig: &MonitorConfig{
Schedule: &intervalSchedule{
Type: "interval",
Value: 1,
Unit: "day",
},
CheckInMargin: 5,
MaxRuntime: 30,
Timezone: "America/Los_Angeles",
},
},
{
Release: "1.0.0",
Environment: "dev",
Type: checkInType,
CheckInID: uuid(),
CheckIn: &CheckIn{
MonitorSlug: "my-monitor",
Status: "ok",
Duration: 10,
},
MonitorConfig: &MonitorConfig{
Schedule: &crontabSchedule{
Type: "crontab",
Value: "0 * * * *",
},
CheckInMargin: 5,
MaxRuntime: 30,
Timezone: "America/Los_Angeles",
},
},
}

var buf bytes.Buffer
enc := json.NewEncoder(&buf)
enc.SetIndent("", " ")
for i, tt := range tests {
i, tt := i, tt
t.Run("", func(t *testing.T) {
defer buf.Reset()
err := enc.Encode(tt)
if err != nil {
t.Fatal(err)
}
path := filepath.Join("testdata", "json", "checkin", fmt.Sprintf("%03d.json", i))
if *update {
WriteGoldenFile(t, path, buf.Bytes())
}
got := buf.String()
want := ReadOrGenerateGoldenFile(t, path, buf.Bytes())
if diff := cmp.Diff(want, got); diff != "" {
t.Fatalf("JSON mismatch (-want +got):\n%s", diff)
}
})
}
}

func TestBreadcrumbMarshalJSON(t *testing.T) {
tests := []*Breadcrumb{
// complete
Expand Down
6 changes: 6 additions & 0 deletions sentry.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ func CaptureException(exception error) *EventID {
return hub.CaptureException(exception)
}

// CaptureCheckIn captures a check in.
func CaptureCheckIn(checkIn *CheckIn, monitorConfig *MonitorConfig) *EventID {
hub := CurrentHub()
return hub.CaptureCheckIn(checkIn, monitorConfig)
}

// CaptureEvent captures an event on the currently active client if any.
//
// The event must already be assembled. Typically code would instead use
Expand Down
8 changes: 8 additions & 0 deletions testdata/json/checkin/000.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"check_in_id": "c2f0ce1334c74564bf6631f6161173f5",
"monitor_slug": "my-monitor",
"status": "ok",
"duration": 10,
"release": "1.0.0",
"environment": "dev"
}
18 changes: 18 additions & 0 deletions testdata/json/checkin/001.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"check_in_id": "0cde0b4e725d4504b2d07c303c2a06d5",
"monitor_slug": "my-monitor",
"status": "ok",
"duration": 10,
"release": "1.0.0",
"environment": "dev",
"monitor_config": {
"schedule": {
"type": "interval",
"value": 1,
"unit": "day"
},
"check_in_margin": 5,
"max_runtime": 30,
"timezone": "America/Los_Angeles"
}
}
17 changes: 17 additions & 0 deletions testdata/json/checkin/002.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"check_in_id": "6fa9f5f4da114025b5bf681d63716f6d",
"monitor_slug": "my-monitor",
"status": "ok",
"duration": 10,
"release": "1.0.0",
"environment": "dev",
"monitor_config": {
"schedule": {
"type": "crontab",
"value": "0 * * * *"
},
"check_in_margin": 5,
"max_runtime": 30,
"timezone": "America/Los_Angeles"
}
}

0 comments on commit 81e5401

Please sign in to comment.