From 3285eb659f0349583a6cdd6b39fb201fc0ac1c6c Mon Sep 17 00:00:00 2001 From: Aram Akhavan Date: Fri, 24 Nov 2023 19:16:48 -0800 Subject: [PATCH 01/10] Fix some development issues --- .gitignore | 4 +++- webapp/backend/pkg/models/testdata/helper.go | 7 ++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 6dad40c..edb4def 100644 --- a/.gitignore +++ b/.gitignore @@ -65,4 +65,6 @@ scrutiny_test.db scrutiny.yaml coverage.txt /config -/influxdb \ No newline at end of file +/influxdb +.angular +web.log \ No newline at end of file diff --git a/webapp/backend/pkg/models/testdata/helper.go b/webapp/backend/pkg/models/testdata/helper.go index 771557c..5e2c623 100644 --- a/webapp/backend/pkg/models/testdata/helper.go +++ b/webapp/backend/pkg/models/testdata/helper.go @@ -4,13 +4,14 @@ import ( "bytes" "encoding/json" "fmt" - "github.com/analogj/scrutiny/webapp/backend/pkg/models/collector" "io" "io/ioutil" "log" "net/http" "os" "time" + + "github.com/analogj/scrutiny/webapp/backend/pkg/models/collector" ) func main() { @@ -32,7 +33,7 @@ func main() { log.Fatalf("ERROR %v", err) } defer file.Close() - _, err = SendPostRequest("http://localhost:9090/api/devices/register", file) + _, err = SendPostRequest("http://localhost:8080/api/devices/register", file) if err != nil { log.Fatalf("ERROR %v", err) } @@ -46,7 +47,7 @@ func main() { log.Fatalf("ERROR %v", err) } - _, err = SendPostRequest(fmt.Sprintf("http://localhost:9090/api/device/%s/smart", diskId), smartDataReader) + _, err = SendPostRequest(fmt.Sprintf("http://localhost:8080/api/device/%s/smart", diskId), smartDataReader) if err != nil { log.Fatalf("ERROR %v", err) } From 6417d713119fc0d392c1048f5187d7b43ff3a45f Mon Sep 17 00:00:00 2001 From: Aram Akhavan Date: Fri, 24 Nov 2023 19:17:06 -0800 Subject: [PATCH 02/10] Add a setting for repeating notifications or not --- .../scrutiny_repository_migrations.go | 20 +++++++++++++++++-- webapp/backend/pkg/models/settings.go | 7 ++++--- .../src/app/core/config/app.config.ts | 4 +++- .../dashboard-settings.component.html | 10 ++++++++++ .../dashboard-settings.component.ts | 5 ++++- 5 files changed, 39 insertions(+), 7 deletions(-) diff --git a/webapp/backend/pkg/database/scrutiny_repository_migrations.go b/webapp/backend/pkg/database/scrutiny_repository_migrations.go index 542677f..e1b948b 100644 --- a/webapp/backend/pkg/database/scrutiny_repository_migrations.go +++ b/webapp/backend/pkg/database/scrutiny_repository_migrations.go @@ -4,6 +4,9 @@ import ( "context" "errors" "fmt" + "strconv" + "time" + "github.com/analogj/scrutiny/webapp/backend/pkg" "github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20201107210306" "github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20220503120000" @@ -17,8 +20,6 @@ import ( "github.com/influxdata/influxdb-client-go/v2/api/http" log "github.com/sirupsen/logrus" "gorm.io/gorm" - "strconv" - "time" ) //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -369,6 +370,21 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error { return tx.Create(&defaultSettings).Error }, }, + { + ID: "m20231123123300", // add repeat_notifications setting. + Migrate: func(tx *gorm.DB) error { + //add repeat_notifications setting default. + var defaultSettings = []m20220716214900.Setting{ + { + SettingKeyName: "metrics.repeat_notifications", + SettingKeyDescription: "Whether to repeat all notifications or just when values change (true | false)", + SettingDataType: "bool", + SettingValueBool: true, + }, + } + return tx.Create(&defaultSettings).Error + }, + }, }) if err := m.Migrate(); err != nil { diff --git a/webapp/backend/pkg/models/settings.go b/webapp/backend/pkg/models/settings.go index f5564ef..e564301 100644 --- a/webapp/backend/pkg/models/settings.go +++ b/webapp/backend/pkg/models/settings.go @@ -17,8 +17,9 @@ type Settings struct { LineStroke string `json:"line_stroke" mapstructure:"line_stroke"` Metrics struct { - NotifyLevel int `json:"notify_level" mapstructure:"notify_level"` - StatusFilterAttributes int `json:"status_filter_attributes" mapstructure:"status_filter_attributes"` - StatusThreshold int `json:"status_threshold" mapstructure:"status_threshold"` + NotifyLevel int `json:"notify_level" mapstructure:"notify_level"` + StatusFilterAttributes int `json:"status_filter_attributes" mapstructure:"status_filter_attributes"` + StatusThreshold int `json:"status_threshold" mapstructure:"status_threshold"` + RepeatNotifications bool `json:"repeat_notifications" mapstructure:"repeat_notifications"` } `json:"metrics" mapstructure:"metrics"` } diff --git a/webapp/frontend/src/app/core/config/app.config.ts b/webapp/frontend/src/app/core/config/app.config.ts index c48d25a..1b6dfe3 100644 --- a/webapp/frontend/src/app/core/config/app.config.ts +++ b/webapp/frontend/src/app/core/config/app.config.ts @@ -55,6 +55,7 @@ export interface AppConfig { notify_level?: MetricsNotifyLevel status_filter_attributes?: MetricsStatusFilterAttributes status_threshold?: MetricsStatusThreshold + repeat_notifications?: boolean } } @@ -82,7 +83,8 @@ export const appConfig: AppConfig = { metrics: { notify_level: MetricsNotifyLevel.Fail, status_filter_attributes: MetricsStatusFilterAttributes.All, - status_threshold: MetricsStatusThreshold.Both + status_threshold: MetricsStatusThreshold.Both, + repeat_notifications: true } }; diff --git a/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.html b/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.html index 69ebd76..0eb9a03 100644 --- a/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.html +++ b/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.html @@ -84,6 +84,16 @@ + +
+ + Repeat Notifications + + Always + Only when the value has changed + + +
diff --git a/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.ts b/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.ts index ea2acce..94f262f 100644 --- a/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.ts +++ b/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.ts @@ -28,6 +28,7 @@ export class DashboardSettingsComponent implements OnInit { theme: string; statusThreshold: number; statusFilterAttributes: number; + repeatNotifications: boolean; // Private private _unsubscribeAll: Subject; @@ -55,6 +56,7 @@ export class DashboardSettingsComponent implements OnInit { this.statusFilterAttributes = config.metrics.status_filter_attributes; this.statusThreshold = config.metrics.status_threshold; + this.repeatNotifications = config.metrics.repeat_notifications; }); @@ -70,7 +72,8 @@ export class DashboardSettingsComponent implements OnInit { theme: this.theme as Theme, metrics: { status_filter_attributes: this.statusFilterAttributes as MetricsStatusFilterAttributes, - status_threshold: this.statusThreshold as MetricsStatusThreshold + status_threshold: this.statusThreshold as MetricsStatusThreshold, + repeat_notifications: this.repeatNotifications } } this._configService.config = newSettings From 4e5c76b2598e4b6ec4841a6382127cf15ec62752 Mon Sep 17 00:00:00 2001 From: Aram Akhavan Date: Sun, 26 Nov 2023 22:43:50 -0800 Subject: [PATCH 03/10] Add support for disabling repeat notifications * Add a new database function for getting the tail * Update ShouldNotify() to handle ignoring repeat notifications if set --- webapp/backend/pkg/database/interface.go | 2 + ...tiny_repository_device_smart_attributes.go | 72 +++++++++++++- webapp/backend/pkg/notify/notify.go | 93 ++++++++++++------- .../pkg/web/handler/upload_device_metrics.go | 6 +- 4 files changed, 138 insertions(+), 35 deletions(-) diff --git a/webapp/backend/pkg/database/interface.go b/webapp/backend/pkg/database/interface.go index f5dae30..94a93d3 100644 --- a/webapp/backend/pkg/database/interface.go +++ b/webapp/backend/pkg/database/interface.go @@ -2,6 +2,7 @@ package database import ( "context" + "github.com/analogj/scrutiny/webapp/backend/pkg" "github.com/analogj/scrutiny/webapp/backend/pkg/models" "github.com/analogj/scrutiny/webapp/backend/pkg/models/collector" @@ -21,6 +22,7 @@ type DeviceRepo interface { SaveSmartAttributes(ctx context.Context, wwn string, collectorSmartData collector.SmartInfo) (measurements.Smart, error) GetSmartAttributeHistory(ctx context.Context, wwn string, durationKey string, attributes []string) ([]measurements.Smart, error) + GetSmartAttributeHistoryTail(ctx context.Context, wwn string, n int, offset int, attributes []string) ([]measurements.Smart, error) SaveSmartTemperature(ctx context.Context, wwn string, deviceProtocol string, collectorSmartData collector.SmartInfo) error diff --git a/webapp/backend/pkg/database/scrutiny_repository_device_smart_attributes.go b/webapp/backend/pkg/database/scrutiny_repository_device_smart_attributes.go index 96015bb..8722163 100644 --- a/webapp/backend/pkg/database/scrutiny_repository_device_smart_attributes.go +++ b/webapp/backend/pkg/database/scrutiny_repository_device_smart_attributes.go @@ -3,13 +3,14 @@ package database import ( "context" "fmt" + "strings" + "time" + "github.com/analogj/scrutiny/webapp/backend/pkg/models/collector" "github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements" influxdb2 "github.com/influxdata/influxdb-client-go/v2" "github.com/influxdata/influxdb-client-go/v2/api" log "github.com/sirupsen/logrus" - "strings" - "time" ) //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -84,6 +85,73 @@ func (sr *scrutinyRepository) GetSmartAttributeHistory(ctx context.Context, wwn } +// GetSmartAttributeHistory MUST return in sorted order, where newest entries are at the beginning of the list, and oldest are at the end. +func (sr *scrutinyRepository) GetSmartAttributeHistoryTail(ctx context.Context, wwn string, n int, offset int, attributes []string) ([]measurements.Smart, error) { + partialQueryStr := []string{ + `import "influxdata/influxdb/schema"`, + } + + nestedDurationKeys := sr.lookupNestedDurationKeys(DURATION_KEY_FOREVER) + subQueryNames := []string{} + for _, nestedDurationKey := range nestedDurationKeys { + bucketName := sr.lookupBucketName(nestedDurationKey) + durationRange := sr.lookupDuration(nestedDurationKey) + + subQueryNames = append(subQueryNames, fmt.Sprintf(`%sData`, nestedDurationKey)) + partialQueryStr = append(partialQueryStr, []string{ + fmt.Sprintf(`%sData = from(bucket: "%s")`, nestedDurationKey, bucketName), + fmt.Sprintf(`|> range(start: %s, stop: %s)`, durationRange[0], durationRange[1]), + `|> filter(fn: (r) => r["_measurement"] == "smart" )`, + fmt.Sprintf(`|> filter(fn: (r) => r["device_wwn"] == "%s" )`, wwn), + // We only need the last `offset` # of entries from each table to guarantee we can + // get the last `n` # of entries starting from `offset` of the union + fmt.Sprintf(`|> tail(n: %d)`, offset), + "|> schema.fieldsAsCols()", + }...) + } + + partialQueryStr = append(partialQueryStr, []string{ + fmt.Sprintf("union(tables: [%s])", strings.Join(subQueryNames, ", ")), + "|> group()", + `|> sort(columns: ["_time"], desc: false)`, + fmt.Sprintf(`|> tail(n: %d, offset: %d)`, n, offset), + `|> yield(name: "last")`, + }...) + + queryStr := strings.Join(partialQueryStr, "\n") + log.Infoln(queryStr) + + smartResults := []measurements.Smart{} + + result, err := sr.influxQueryApi.Query(ctx, queryStr) + if err == nil { + // Use Next() to iterate over query result lines + for result.Next() { + // Observe when there is new grouping key producing new table + if result.TableChanged() { + //fmt.Printf("table: %s\n", result.TableMetadata().String()) + } + + smartData, err := measurements.NewSmartFromInfluxDB(result.Record().Values()) + if err != nil { + return nil, err + } + smartResults = append(smartResults, *smartData) + + } + if result.Err() != nil { + fmt.Printf("Query error: %s\n", result.Err().Error()) + } + } else { + return nil, err + } + + //we have to sort the smartResults again, because the `union` command will return multiple 'tables' and only sort the records in each table. + sortSmartMeasurementsDesc(smartResults) + + return smartResults, nil +} + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Helper Methods //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/webapp/backend/pkg/notify/notify.go b/webapp/backend/pkg/notify/notify.go index 4a1862e..d04ddd5 100644 --- a/webapp/backend/pkg/notify/notify.go +++ b/webapp/backend/pkg/notify/notify.go @@ -15,11 +15,13 @@ import ( "github.com/analogj/go-util/utils" "github.com/analogj/scrutiny/webapp/backend/pkg" "github.com/analogj/scrutiny/webapp/backend/pkg/config" + "github.com/analogj/scrutiny/webapp/backend/pkg/database" "github.com/analogj/scrutiny/webapp/backend/pkg/models" "github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements" "github.com/analogj/scrutiny/webapp/backend/pkg/thresholds" "github.com/containrrr/shoutrrr" shoutrrrTypes "github.com/containrrr/shoutrrr/pkg/types" + "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" "golang.org/x/sync/errgroup" ) @@ -30,7 +32,7 @@ const NotifyFailureTypeSmartFailure = "SmartFailure" const NotifyFailureTypeScrutinyFailure = "ScrutinyFailure" // ShouldNotify check if the error Message should be filtered (level mismatch or filtered_attributes) -func ShouldNotify(device models.Device, smartAttrs measurements.Smart, statusThreshold pkg.MetricsStatusThreshold, statusFilterAttributes pkg.MetricsStatusFilterAttributes) bool { +func ShouldNotify(device models.Device, smartAttrs measurements.Smart, statusThreshold pkg.MetricsStatusThreshold, statusFilterAttributes pkg.MetricsStatusFilterAttributes, repeatNotifications bool, c *gin.Context, deviceRepo database.DeviceRepo) bool { // 1. check if the device is healthy if device.DeviceStatus == pkg.DeviceStatusPassed { return false @@ -54,52 +56,79 @@ func ShouldNotify(device models.Device, smartAttrs measurements.Smart, statusThr requiredAttrStatus = pkg.AttributeStatusFailedScrutiny } - // 2. check if the attributes that are failing should be filtered (non-critical) - // 3. for any unfiltered attribute, store the failure reason (Smart or Scrutiny) - if statusFilterAttributes == pkg.MetricsStatusFilterAttributesCritical { - hasFailingCriticalAttr := false - var statusFailingCriticalAttr pkg.AttributeStatus - - for attrId, attrData := range smartAttrs.Attributes { - //find failing attribute - if attrData.GetStatus() == pkg.AttributeStatusPassed { - continue //skip all passing attributes - } + // This is the only case where individual attributes need not be considered + if statusFilterAttributes == pkg.MetricsStatusFilterAttributesAll && repeatNotifications { + return pkg.DeviceStatusHas(device.DeviceStatus, requiredDeviceStatus) + } - // merge the status's of all critical attributes - statusFailingCriticalAttr = pkg.AttributeStatusSet(statusFailingCriticalAttr, attrData.GetStatus()) + var failingAttributes []string + for attrId, attrData := range smartAttrs.Attributes { + var status pkg.AttributeStatus = attrData.GetStatus() + if status == pkg.AttributeStatusPassed { + continue + } - //found a failing attribute, see if its critical - if device.IsScsi() && thresholds.ScsiMetadata[attrId].Critical { - hasFailingCriticalAttr = true - } else if device.IsNvme() && thresholds.NmveMetadata[attrId].Critical { - hasFailingCriticalAttr = true + if statusFilterAttributes == pkg.MetricsStatusFilterAttributesCritical { + critical := false + if device.IsScsi() { + critical = thresholds.ScsiMetadata[attrId].Critical + } else if device.IsNvme() { + critical = thresholds.NmveMetadata[attrId].Critical } else { //this is ATA attrIdInt, err := strconv.Atoi(attrId) if err != nil { continue } - if thresholds.AtaMetadata[attrIdInt].Critical { - hasFailingCriticalAttr = true - } + critical = thresholds.AtaMetadata[attrIdInt].Critical + } + if !critical { + continue } - } - if !hasFailingCriticalAttr { - //no critical attributes are failing, and notifyFilterAttributes == "critical" + failingAttributes = append(failingAttributes, attrId) + } + + if repeatNotifications { + lastPoints, err := deviceRepo.GetSmartAttributeHistoryTail(c, c.Param("wwn"), 1, 1, failingAttributes) + if err == nil && len(lastPoints) > 1 { + for _, attrId := range failingAttributes { + if old, ok := lastPoints[0].Attributes[attrId].(*measurements.SmartAtaAttribute); ok { + if current, ok := smartAttrs.Attributes[attrId].(*measurements.SmartAtaAttribute); ok { + if old.TransformedValue != current.TransformedValue { + return true + } + } + } + if old, ok := lastPoints[0].Attributes[attrId].(*measurements.SmartNvmeAttribute); ok { + if current, ok := smartAttrs.Attributes[attrId].(*measurements.SmartNvmeAttribute); ok { + if old.TransformedValue != current.TransformedValue { + return true + } + } + } + if old, ok := lastPoints[0].Attributes[attrId].(*measurements.SmartScsiAttribute); ok { + if current, ok := smartAttrs.Attributes[attrId].(*measurements.SmartScsiAttribute); ok { + if old.TransformedValue != current.TransformedValue { + return true + } + } + } + } return false - } else { - // check if any of the critical attributes have a status that we're looking for - return pkg.AttributeStatusHas(statusFailingCriticalAttr, requiredAttrStatus) } - + return true } else { - // 2. SKIP - we are processing every attribute. - // 3. check if the device failure level matches the wanted failure level. - return pkg.DeviceStatusHas(device.DeviceStatus, requiredDeviceStatus) + for _, attr := range failingAttributes { + attrStatus := smartAttrs.Attributes[attr].GetStatus() + if pkg.AttributeStatusHas(attrStatus, requiredAttrStatus) { + return true + } + } } + + return false } // TODO: include user label for device. diff --git a/webapp/backend/pkg/web/handler/upload_device_metrics.go b/webapp/backend/pkg/web/handler/upload_device_metrics.go index f58d6ed..f83107f 100644 --- a/webapp/backend/pkg/web/handler/upload_device_metrics.go +++ b/webapp/backend/pkg/web/handler/upload_device_metrics.go @@ -2,6 +2,8 @@ package handler import ( "fmt" + "net/http" + "github.com/analogj/scrutiny/webapp/backend/pkg" "github.com/analogj/scrutiny/webapp/backend/pkg/config" "github.com/analogj/scrutiny/webapp/backend/pkg/database" @@ -9,7 +11,6 @@ import ( "github.com/analogj/scrutiny/webapp/backend/pkg/notify" "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" - "net/http" ) func UploadDeviceMetrics(c *gin.Context) { @@ -73,6 +74,9 @@ func UploadDeviceMetrics(c *gin.Context) { smartData, pkg.MetricsStatusThreshold(appConfig.GetInt(fmt.Sprintf("%s.metrics.status_threshold", config.DB_USER_SETTINGS_SUBKEY))), pkg.MetricsStatusFilterAttributes(appConfig.GetInt(fmt.Sprintf("%s.metrics.status_filter_attributes", config.DB_USER_SETTINGS_SUBKEY))), + appConfig.GetBool(fmt.Sprintf("%s.metrics.repeat_notifications", config.DB_USER_SETTINGS_SUBKEY)), + c, + deviceRepo, ) { //send notifications From 98d958888c3efb4571a76a06bf7ec0f7c0ec82d8 Mon Sep 17 00:00:00 2001 From: Aram Akhavan Date: Sun, 3 Dec 2023 22:17:29 -0800 Subject: [PATCH 04/10] refactor common code --- webapp/backend/pkg/database/helpers.go | 12 -- webapp/backend/pkg/database/helpers_test.go | 30 ---- webapp/backend/pkg/database/interface.go | 3 +- ...tiny_repository_device_smart_attributes.go | 151 +++++++----------- .../measurements/smart_ata_attribute.go | 9 +- .../models/measurements/smart_attribute.go | 1 + .../measurements/smart_nvme_attribute.go | 7 +- .../measurements/smart_scsci_attribute.go | 7 +- webapp/backend/pkg/notify/notify.go | 28 +--- .../pkg/web/handler/get_device_details.go | 5 +- 10 files changed, 84 insertions(+), 169 deletions(-) delete mode 100644 webapp/backend/pkg/database/helpers.go delete mode 100644 webapp/backend/pkg/database/helpers_test.go diff --git a/webapp/backend/pkg/database/helpers.go b/webapp/backend/pkg/database/helpers.go deleted file mode 100644 index 3706d86..0000000 --- a/webapp/backend/pkg/database/helpers.go +++ /dev/null @@ -1,12 +0,0 @@ -package database - -import ( - "github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements" - "sort" -) - -func sortSmartMeasurementsDesc(smartResults []measurements.Smart) { - sort.SliceStable(smartResults, func(i, j int) bool { - return smartResults[i].Date.After(smartResults[j].Date) - }) -} diff --git a/webapp/backend/pkg/database/helpers_test.go b/webapp/backend/pkg/database/helpers_test.go deleted file mode 100644 index 38587b1..0000000 --- a/webapp/backend/pkg/database/helpers_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package database - -import ( - "github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements" - "github.com/stretchr/testify/require" - "testing" - "time" -) - -func Test_sortSmartMeasurementsDesc_LatestFirst(t *testing.T) { - //setup - timeNow := time.Now() - smartResults := []measurements.Smart{ - { - Date: timeNow.AddDate(0, 0, -2), - }, - { - Date: timeNow, - }, - { - Date: timeNow.AddDate(0, 0, -1), - }, - } - - //test - sortSmartMeasurementsDesc(smartResults) - - //assert - require.Equal(t, smartResults[0].Date, timeNow) -} diff --git a/webapp/backend/pkg/database/interface.go b/webapp/backend/pkg/database/interface.go index 94a93d3..e72d2a9 100644 --- a/webapp/backend/pkg/database/interface.go +++ b/webapp/backend/pkg/database/interface.go @@ -21,8 +21,7 @@ type DeviceRepo interface { DeleteDevice(ctx context.Context, wwn string) error SaveSmartAttributes(ctx context.Context, wwn string, collectorSmartData collector.SmartInfo) (measurements.Smart, error) - GetSmartAttributeHistory(ctx context.Context, wwn string, durationKey string, attributes []string) ([]measurements.Smart, error) - GetSmartAttributeHistoryTail(ctx context.Context, wwn string, n int, offset int, attributes []string) ([]measurements.Smart, error) + GetSmartAttributeHistory(ctx context.Context, wwn string, durationKey string, n int, offset int, attributes []string) ([]measurements.Smart, error) SaveSmartTemperature(ctx context.Context, wwn string, deviceProtocol string, collectorSmartData collector.SmartInfo) error diff --git a/webapp/backend/pkg/database/scrutiny_repository_device_smart_attributes.go b/webapp/backend/pkg/database/scrutiny_repository_device_smart_attributes.go index 8722163..7a8a194 100644 --- a/webapp/backend/pkg/database/scrutiny_repository_device_smart_attributes.go +++ b/webapp/backend/pkg/database/scrutiny_repository_device_smart_attributes.go @@ -31,14 +31,14 @@ func (sr *scrutinyRepository) SaveSmartAttributes(ctx context.Context, wwn strin } // GetSmartAttributeHistory MUST return in sorted order, where newest entries are at the beginning of the list, and oldest are at the end. -func (sr *scrutinyRepository) GetSmartAttributeHistory(ctx context.Context, wwn string, durationKey string, attributes []string) ([]measurements.Smart, error) { +func (sr *scrutinyRepository) GetSmartAttributeHistory(ctx context.Context, wwn string, durationKey string, n int, offset int, attributes []string) ([]measurements.Smart, error) { // Get SMartResults from InfluxDB //TODO: change the filter startrange to a real number. // Get parser flux query result //appConfig.GetString("web.influxdb.bucket") - queryStr := sr.aggregateSmartAttributesQuery(wwn, durationKey) + queryStr := sr.aggregateSmartAttributesQuery(wwn, durationKey, n, offset, attributes) log.Infoln(queryStr) smartResults := []measurements.Smart{} @@ -66,9 +66,6 @@ func (sr *scrutinyRepository) GetSmartAttributeHistory(ctx context.Context, wwn return nil, err } - //we have to sort the smartResults again, because the `union` command will return multiple 'tables' and only sort the records in each table. - sortSmartMeasurementsDesc(smartResults) - return smartResults, nil //if err := device.SquashHistory(); err != nil { @@ -85,73 +82,6 @@ func (sr *scrutinyRepository) GetSmartAttributeHistory(ctx context.Context, wwn } -// GetSmartAttributeHistory MUST return in sorted order, where newest entries are at the beginning of the list, and oldest are at the end. -func (sr *scrutinyRepository) GetSmartAttributeHistoryTail(ctx context.Context, wwn string, n int, offset int, attributes []string) ([]measurements.Smart, error) { - partialQueryStr := []string{ - `import "influxdata/influxdb/schema"`, - } - - nestedDurationKeys := sr.lookupNestedDurationKeys(DURATION_KEY_FOREVER) - subQueryNames := []string{} - for _, nestedDurationKey := range nestedDurationKeys { - bucketName := sr.lookupBucketName(nestedDurationKey) - durationRange := sr.lookupDuration(nestedDurationKey) - - subQueryNames = append(subQueryNames, fmt.Sprintf(`%sData`, nestedDurationKey)) - partialQueryStr = append(partialQueryStr, []string{ - fmt.Sprintf(`%sData = from(bucket: "%s")`, nestedDurationKey, bucketName), - fmt.Sprintf(`|> range(start: %s, stop: %s)`, durationRange[0], durationRange[1]), - `|> filter(fn: (r) => r["_measurement"] == "smart" )`, - fmt.Sprintf(`|> filter(fn: (r) => r["device_wwn"] == "%s" )`, wwn), - // We only need the last `offset` # of entries from each table to guarantee we can - // get the last `n` # of entries starting from `offset` of the union - fmt.Sprintf(`|> tail(n: %d)`, offset), - "|> schema.fieldsAsCols()", - }...) - } - - partialQueryStr = append(partialQueryStr, []string{ - fmt.Sprintf("union(tables: [%s])", strings.Join(subQueryNames, ", ")), - "|> group()", - `|> sort(columns: ["_time"], desc: false)`, - fmt.Sprintf(`|> tail(n: %d, offset: %d)`, n, offset), - `|> yield(name: "last")`, - }...) - - queryStr := strings.Join(partialQueryStr, "\n") - log.Infoln(queryStr) - - smartResults := []measurements.Smart{} - - result, err := sr.influxQueryApi.Query(ctx, queryStr) - if err == nil { - // Use Next() to iterate over query result lines - for result.Next() { - // Observe when there is new grouping key producing new table - if result.TableChanged() { - //fmt.Printf("table: %s\n", result.TableMetadata().String()) - } - - smartData, err := measurements.NewSmartFromInfluxDB(result.Record().Values()) - if err != nil { - return nil, err - } - smartResults = append(smartResults, *smartData) - - } - if result.Err() != nil { - fmt.Printf("Query error: %s\n", result.Err().Error()) - } - } else { - return nil, err - } - - //we have to sort the smartResults again, because the `union` command will return multiple 'tables' and only sort the records in each table. - sortSmartMeasurementsDesc(smartResults) - - return smartResults, nil -} - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Helper Methods //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -167,7 +97,7 @@ func (sr *scrutinyRepository) saveDatapoint(influxWriteApi api.WriteAPIBlocking, return influxWriteApi.WritePoint(ctx, p) } -func (sr *scrutinyRepository) aggregateSmartAttributesQuery(wwn string, durationKey string) string { +func (sr *scrutinyRepository) aggregateSmartAttributesQuery(wwn string, durationKey string, n int, offset int, attributes []string) string { /* @@ -176,28 +106,34 @@ func (sr *scrutinyRepository) aggregateSmartAttributesQuery(wwn string, duration |> range(start: -1w, stop: now()) |> filter(fn: (r) => r["_measurement"] == "smart" ) |> filter(fn: (r) => r["device_wwn"] == "0x5000c5002df89099" ) + |> tail(n: 10, offset: 0) |> schema.fieldsAsCols() monthData = from(bucket: "metrics_weekly") |> range(start: -1mo, stop: -1w) |> filter(fn: (r) => r["_measurement"] == "smart" ) |> filter(fn: (r) => r["device_wwn"] == "0x5000c5002df89099" ) + |> tail(n: 10, offset: 0) |> schema.fieldsAsCols() yearData = from(bucket: "metrics_monthly") |> range(start: -1y, stop: -1mo) |> filter(fn: (r) => r["_measurement"] == "smart" ) |> filter(fn: (r) => r["device_wwn"] == "0x5000c5002df89099" ) + |> tail(n: 10, offset: 0) |> schema.fieldsAsCols() foreverData = from(bucket: "metrics_yearly") |> range(start: -10y, stop: -1y) |> filter(fn: (r) => r["_measurement"] == "smart" ) |> filter(fn: (r) => r["device_wwn"] == "0x5000c5002df89099" ) + |> tail(n: 10, offset: 0) |> schema.fieldsAsCols() union(tables: [weekData, monthData, yearData, foreverData]) - |> sort(columns: ["_time"], desc: false) + |> group() + |> sort(columns: ["_time"], desc: true) + |> tail(n: 6, offset: 4) |> yield(name: "last") */ @@ -208,34 +144,57 @@ func (sr *scrutinyRepository) aggregateSmartAttributesQuery(wwn string, duration nestedDurationKeys := sr.lookupNestedDurationKeys(durationKey) + if len(nestedDurationKeys) == 1 { + //there's only one bucket being queried, no need to union, just aggregate the dataset and return + partialQueryStr = append(partialQueryStr, []string{ + sr.generateSmartAttributesSubquery(wwn, nestedDurationKeys[0], n, offset, attributes), + fmt.Sprintf(`%sData`, nestedDurationKeys[0]), + `|> sort(columns: ["_time"], desc: true)`, + `|> yield()`, + }...) + return strings.Join(partialQueryStr, "\n") + } + + subQueries := []string{} subQueryNames := []string{} for _, nestedDurationKey := range nestedDurationKeys { - bucketName := sr.lookupBucketName(nestedDurationKey) - durationRange := sr.lookupDuration(nestedDurationKey) - subQueryNames = append(subQueryNames, fmt.Sprintf(`%sData`, nestedDurationKey)) - partialQueryStr = append(partialQueryStr, []string{ - fmt.Sprintf(`%sData = from(bucket: "%s")`, nestedDurationKey, bucketName), - fmt.Sprintf(`|> range(start: %s, stop: %s)`, durationRange[0], durationRange[1]), - `|> filter(fn: (r) => r["_measurement"] == "smart" )`, - fmt.Sprintf(`|> filter(fn: (r) => r["device_wwn"] == "%s" )`, wwn), - "|> schema.fieldsAsCols()", - }...) + if n > 0 { + // We only need the last `n + offset` # of entries from each table to guarantee we can + // get the last `n` # of entries starting from `offset` of the union + subQueries = append(subQueries, sr.generateSmartAttributesSubquery(wwn, nestedDurationKey, n+offset, 0, attributes)) + } else { + subQueries = append(subQueries, sr.generateSmartAttributesSubquery(wwn, nestedDurationKey, 0, 0, attributes)) + } + } + partialQueryStr = append(partialQueryStr, subQueries...) + partialQueryStr = append(partialQueryStr, []string{ + fmt.Sprintf("union(tables: [%s])", strings.Join(subQueryNames, ", ")), + `|> group()`, + `|> sort(columns: ["_time"], desc: true)`, + }...) + if n > 0 { + partialQueryStr = append(partialQueryStr, fmt.Sprintf(`|> tail(n: %d, offset: %d)`, n, offset)) } + partialQueryStr = append(partialQueryStr, `|> yield(name: "last")`) - if len(subQueryNames) == 1 { - //there's only one bucket being queried, no need to union, just aggregate the dataset and return - partialQueryStr = append(partialQueryStr, []string{ - subQueryNames[0], - `|> yield()`, - }...) - } else { - partialQueryStr = append(partialQueryStr, []string{ - fmt.Sprintf("union(tables: [%s])", strings.Join(subQueryNames, ", ")), - `|> sort(columns: ["_time"], desc: false)`, - `|> yield(name: "last")`, - }...) + return strings.Join(partialQueryStr, "\n") +} + +func (sr *scrutinyRepository) generateSmartAttributesSubquery(wwn string, durationKey string, n int, offset int, attributes []string) string { + bucketName := sr.lookupBucketName(durationKey) + durationRange := sr.lookupDuration(durationKey) + + partialQueryStr := []string{ + fmt.Sprintf(`%sData = from(bucket: "%s")`, durationKey, bucketName), + fmt.Sprintf(`|> range(start: %s, stop: %s)`, durationRange[0], durationRange[1]), + `|> filter(fn: (r) => r["_measurement"] == "smart" )`, + fmt.Sprintf(`|> filter(fn: (r) => r["device_wwn"] == "%s" )`, wwn), + } + if n > 0 { + partialQueryStr = append(partialQueryStr, fmt.Sprintf(`|> tail(n: %d, offset: %d)`, n, offset)) } + partialQueryStr = append(partialQueryStr, "|> schema.fieldsAsCols()") return strings.Join(partialQueryStr, "\n") } diff --git a/webapp/backend/pkg/models/measurements/smart_ata_attribute.go b/webapp/backend/pkg/models/measurements/smart_ata_attribute.go index 8b9a3f4..c1ace9c 100644 --- a/webapp/backend/pkg/models/measurements/smart_ata_attribute.go +++ b/webapp/backend/pkg/models/measurements/smart_ata_attribute.go @@ -2,10 +2,11 @@ package measurements import ( "fmt" - "github.com/analogj/scrutiny/webapp/backend/pkg" - "github.com/analogj/scrutiny/webapp/backend/pkg/thresholds" "strconv" "strings" + + "github.com/analogj/scrutiny/webapp/backend/pkg" + "github.com/analogj/scrutiny/webapp/backend/pkg/thresholds" ) type SmartAtaAttribute struct { @@ -24,6 +25,10 @@ type SmartAtaAttribute struct { FailureRate float64 `json:"failure_rate,omitempty"` } +func (sa *SmartAtaAttribute) GetTransformedValue() int64 { + return sa.TransformedValue +} + func (sa *SmartAtaAttribute) GetStatus() pkg.AttributeStatus { return sa.Status } diff --git a/webapp/backend/pkg/models/measurements/smart_attribute.go b/webapp/backend/pkg/models/measurements/smart_attribute.go index a8de369..d885acf 100644 --- a/webapp/backend/pkg/models/measurements/smart_attribute.go +++ b/webapp/backend/pkg/models/measurements/smart_attribute.go @@ -6,4 +6,5 @@ type SmartAttribute interface { Flatten() (fields map[string]interface{}) Inflate(key string, val interface{}) GetStatus() pkg.AttributeStatus + GetTransformedValue() int64 } diff --git a/webapp/backend/pkg/models/measurements/smart_nvme_attribute.go b/webapp/backend/pkg/models/measurements/smart_nvme_attribute.go index a2c58a1..4e251d0 100644 --- a/webapp/backend/pkg/models/measurements/smart_nvme_attribute.go +++ b/webapp/backend/pkg/models/measurements/smart_nvme_attribute.go @@ -2,9 +2,10 @@ package measurements import ( "fmt" + "strings" + "github.com/analogj/scrutiny/webapp/backend/pkg" "github.com/analogj/scrutiny/webapp/backend/pkg/thresholds" - "strings" ) type SmartNvmeAttribute struct { @@ -18,6 +19,10 @@ type SmartNvmeAttribute struct { FailureRate float64 `json:"failure_rate,omitempty"` } +func (sa *SmartNvmeAttribute) GetTransformedValue() int64 { + return sa.TransformedValue +} + func (sa *SmartNvmeAttribute) GetStatus() pkg.AttributeStatus { return sa.Status } diff --git a/webapp/backend/pkg/models/measurements/smart_scsci_attribute.go b/webapp/backend/pkg/models/measurements/smart_scsci_attribute.go index 4fd8a36..347a3f6 100644 --- a/webapp/backend/pkg/models/measurements/smart_scsci_attribute.go +++ b/webapp/backend/pkg/models/measurements/smart_scsci_attribute.go @@ -2,9 +2,10 @@ package measurements import ( "fmt" + "strings" + "github.com/analogj/scrutiny/webapp/backend/pkg" "github.com/analogj/scrutiny/webapp/backend/pkg/thresholds" - "strings" ) type SmartScsiAttribute struct { @@ -18,6 +19,10 @@ type SmartScsiAttribute struct { FailureRate float64 `json:"failure_rate,omitempty"` } +func (sa *SmartScsiAttribute) GetTransformedValue() int64 { + return sa.TransformedValue +} + func (sa *SmartScsiAttribute) GetStatus() pkg.AttributeStatus { return sa.Status } diff --git a/webapp/backend/pkg/notify/notify.go b/webapp/backend/pkg/notify/notify.go index d04ddd5..aac6cad 100644 --- a/webapp/backend/pkg/notify/notify.go +++ b/webapp/backend/pkg/notify/notify.go @@ -90,30 +90,12 @@ func ShouldNotify(device models.Device, smartAttrs measurements.Smart, statusThr failingAttributes = append(failingAttributes, attrId) } - if repeatNotifications { - lastPoints, err := deviceRepo.GetSmartAttributeHistoryTail(c, c.Param("wwn"), 1, 1, failingAttributes) + if !repeatNotifications { + lastPoints, err := deviceRepo.GetSmartAttributeHistory(c, c.Param("wwn"), database.DURATION_KEY_FOREVER, 1, 1, failingAttributes) if err == nil && len(lastPoints) > 1 { for _, attrId := range failingAttributes { - if old, ok := lastPoints[0].Attributes[attrId].(*measurements.SmartAtaAttribute); ok { - if current, ok := smartAttrs.Attributes[attrId].(*measurements.SmartAtaAttribute); ok { - if old.TransformedValue != current.TransformedValue { - return true - } - } - } - if old, ok := lastPoints[0].Attributes[attrId].(*measurements.SmartNvmeAttribute); ok { - if current, ok := smartAttrs.Attributes[attrId].(*measurements.SmartNvmeAttribute); ok { - if old.TransformedValue != current.TransformedValue { - return true - } - } - } - if old, ok := lastPoints[0].Attributes[attrId].(*measurements.SmartScsiAttribute); ok { - if current, ok := smartAttrs.Attributes[attrId].(*measurements.SmartScsiAttribute); ok { - if old.TransformedValue != current.TransformedValue { - return true - } - } + if lastPoints[0].Attributes[attrId].GetTransformedValue() != smartAttrs.Attributes[attrId].GetTransformedValue() { + return true } } return false @@ -251,7 +233,7 @@ func (n *Notify) Send() error { notifyScripts := []string{} notifyShoutrrr := []string{} - for ndx, _ := range configUrls { + for ndx := range configUrls { if strings.HasPrefix(configUrls[ndx], "https://") || strings.HasPrefix(configUrls[ndx], "http://") { notifyWebhooks = append(notifyWebhooks, configUrls[ndx]) } else if strings.HasPrefix(configUrls[ndx], "script://") { diff --git a/webapp/backend/pkg/web/handler/get_device_details.go b/webapp/backend/pkg/web/handler/get_device_details.go index b4e24ee..49e48a1 100644 --- a/webapp/backend/pkg/web/handler/get_device_details.go +++ b/webapp/backend/pkg/web/handler/get_device_details.go @@ -1,11 +1,12 @@ package handler import ( + "net/http" + "github.com/analogj/scrutiny/webapp/backend/pkg/database" "github.com/analogj/scrutiny/webapp/backend/pkg/thresholds" "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" - "net/http" ) func GetDeviceDetails(c *gin.Context) { @@ -24,7 +25,7 @@ func GetDeviceDetails(c *gin.Context) { durationKey = "forever" } - smartResults, err := deviceRepo.GetSmartAttributeHistory(c, c.Param("wwn"), durationKey, nil) + smartResults, err := deviceRepo.GetSmartAttributeHistory(c, c.Param("wwn"), durationKey, 0, 0, nil) if err != nil { logger.Errorln("An error occurred while retrieving device smart results", err) c.JSON(http.StatusInternalServerError, gin.H{"success": false}) From 01c5a7fdfe7191569afcee3f0dc01ffe9f9881f6 Mon Sep 17 00:00:00 2001 From: Aram Akhavan Date: Fri, 8 Dec 2023 11:18:13 -0800 Subject: [PATCH 05/10] Address review comments --- webapp/backend/pkg/database/interface.go | 2 +- ...tiny_repository_device_smart_attributes.go | 25 ++++++------ webapp/backend/pkg/notify/notify.go | 38 +++++++++++-------- .../pkg/web/handler/upload_device_metrics.go | 1 + 4 files changed, 39 insertions(+), 27 deletions(-) diff --git a/webapp/backend/pkg/database/interface.go b/webapp/backend/pkg/database/interface.go index e72d2a9..2b34a1f 100644 --- a/webapp/backend/pkg/database/interface.go +++ b/webapp/backend/pkg/database/interface.go @@ -21,7 +21,7 @@ type DeviceRepo interface { DeleteDevice(ctx context.Context, wwn string) error SaveSmartAttributes(ctx context.Context, wwn string, collectorSmartData collector.SmartInfo) (measurements.Smart, error) - GetSmartAttributeHistory(ctx context.Context, wwn string, durationKey string, n int, offset int, attributes []string) ([]measurements.Smart, error) + GetSmartAttributeHistory(ctx context.Context, wwn string, durationKey string, selectEntries int, selectEntriesOffset int, attributes []string) ([]measurements.Smart, error) SaveSmartTemperature(ctx context.Context, wwn string, deviceProtocol string, collectorSmartData collector.SmartInfo) error diff --git a/webapp/backend/pkg/database/scrutiny_repository_device_smart_attributes.go b/webapp/backend/pkg/database/scrutiny_repository_device_smart_attributes.go index 7a8a194..b1abf6c 100644 --- a/webapp/backend/pkg/database/scrutiny_repository_device_smart_attributes.go +++ b/webapp/backend/pkg/database/scrutiny_repository_device_smart_attributes.go @@ -31,14 +31,17 @@ func (sr *scrutinyRepository) SaveSmartAttributes(ctx context.Context, wwn strin } // GetSmartAttributeHistory MUST return in sorted order, where newest entries are at the beginning of the list, and oldest are at the end. -func (sr *scrutinyRepository) GetSmartAttributeHistory(ctx context.Context, wwn string, durationKey string, n int, offset int, attributes []string) ([]measurements.Smart, error) { +// When selectEntries is > 0, only the most recent selectEntries database entries are returned, starting from the selectEntriesOffset entry. +// For example, with selectEntries = 5, selectEntries = 0, the most recent 5 are returned. With selectEntries = 3, selectEntries = 2, entries +// 2 to 4 are returned (2 being the third newest, since it is zero-indexed) +func (sr *scrutinyRepository) GetSmartAttributeHistory(ctx context.Context, wwn string, durationKey string, selectEntries int, selectEntriesOffset int, attributes []string) ([]measurements.Smart, error) { // Get SMartResults from InfluxDB //TODO: change the filter startrange to a real number. // Get parser flux query result //appConfig.GetString("web.influxdb.bucket") - queryStr := sr.aggregateSmartAttributesQuery(wwn, durationKey, n, offset, attributes) + queryStr := sr.aggregateSmartAttributesQuery(wwn, durationKey, selectEntries, selectEntriesOffset, attributes) log.Infoln(queryStr) smartResults := []measurements.Smart{} @@ -97,7 +100,7 @@ func (sr *scrutinyRepository) saveDatapoint(influxWriteApi api.WriteAPIBlocking, return influxWriteApi.WritePoint(ctx, p) } -func (sr *scrutinyRepository) aggregateSmartAttributesQuery(wwn string, durationKey string, n int, offset int, attributes []string) string { +func (sr *scrutinyRepository) aggregateSmartAttributesQuery(wwn string, durationKey string, selectEntries int, selectEntriesOffset int, attributes []string) string { /* @@ -147,7 +150,7 @@ func (sr *scrutinyRepository) aggregateSmartAttributesQuery(wwn string, duration if len(nestedDurationKeys) == 1 { //there's only one bucket being queried, no need to union, just aggregate the dataset and return partialQueryStr = append(partialQueryStr, []string{ - sr.generateSmartAttributesSubquery(wwn, nestedDurationKeys[0], n, offset, attributes), + sr.generateSmartAttributesSubquery(wwn, nestedDurationKeys[0], selectEntries, selectEntriesOffset, attributes), fmt.Sprintf(`%sData`, nestedDurationKeys[0]), `|> sort(columns: ["_time"], desc: true)`, `|> yield()`, @@ -159,10 +162,10 @@ func (sr *scrutinyRepository) aggregateSmartAttributesQuery(wwn string, duration subQueryNames := []string{} for _, nestedDurationKey := range nestedDurationKeys { subQueryNames = append(subQueryNames, fmt.Sprintf(`%sData`, nestedDurationKey)) - if n > 0 { + if selectEntries > 0 { // We only need the last `n + offset` # of entries from each table to guarantee we can // get the last `n` # of entries starting from `offset` of the union - subQueries = append(subQueries, sr.generateSmartAttributesSubquery(wwn, nestedDurationKey, n+offset, 0, attributes)) + subQueries = append(subQueries, sr.generateSmartAttributesSubquery(wwn, nestedDurationKey, selectEntries+selectEntriesOffset, 0, attributes)) } else { subQueries = append(subQueries, sr.generateSmartAttributesSubquery(wwn, nestedDurationKey, 0, 0, attributes)) } @@ -173,15 +176,15 @@ func (sr *scrutinyRepository) aggregateSmartAttributesQuery(wwn string, duration `|> group()`, `|> sort(columns: ["_time"], desc: true)`, }...) - if n > 0 { - partialQueryStr = append(partialQueryStr, fmt.Sprintf(`|> tail(n: %d, offset: %d)`, n, offset)) + if selectEntries > 0 { + partialQueryStr = append(partialQueryStr, fmt.Sprintf(`|> tail(n: %d, offset: %d)`, selectEntries, selectEntriesOffset)) } partialQueryStr = append(partialQueryStr, `|> yield(name: "last")`) return strings.Join(partialQueryStr, "\n") } -func (sr *scrutinyRepository) generateSmartAttributesSubquery(wwn string, durationKey string, n int, offset int, attributes []string) string { +func (sr *scrutinyRepository) generateSmartAttributesSubquery(wwn string, durationKey string, selectEntries int, selectEntriesOffset int, attributes []string) string { bucketName := sr.lookupBucketName(durationKey) durationRange := sr.lookupDuration(durationKey) @@ -191,8 +194,8 @@ func (sr *scrutinyRepository) generateSmartAttributesSubquery(wwn string, durati `|> filter(fn: (r) => r["_measurement"] == "smart" )`, fmt.Sprintf(`|> filter(fn: (r) => r["device_wwn"] == "%s" )`, wwn), } - if n > 0 { - partialQueryStr = append(partialQueryStr, fmt.Sprintf(`|> tail(n: %d, offset: %d)`, n, offset)) + if selectEntries > 0 { + partialQueryStr = append(partialQueryStr, fmt.Sprintf(`|> tail(n: %d, offset: %d)`, selectEntries, selectEntriesOffset)) } partialQueryStr = append(partialQueryStr, "|> schema.fieldsAsCols()") diff --git a/webapp/backend/pkg/notify/notify.go b/webapp/backend/pkg/notify/notify.go index aac6cad..bf927ee 100644 --- a/webapp/backend/pkg/notify/notify.go +++ b/webapp/backend/pkg/notify/notify.go @@ -32,7 +32,7 @@ const NotifyFailureTypeSmartFailure = "SmartFailure" const NotifyFailureTypeScrutinyFailure = "ScrutinyFailure" // ShouldNotify check if the error Message should be filtered (level mismatch or filtered_attributes) -func ShouldNotify(device models.Device, smartAttrs measurements.Smart, statusThreshold pkg.MetricsStatusThreshold, statusFilterAttributes pkg.MetricsStatusFilterAttributes, repeatNotifications bool, c *gin.Context, deviceRepo database.DeviceRepo) bool { +func ShouldNotify(logger logrus.FieldLogger, device models.Device, smartAttrs measurements.Smart, statusThreshold pkg.MetricsStatusThreshold, statusFilterAttributes pkg.MetricsStatusFilterAttributes, repeatNotifications bool, c *gin.Context, deviceRepo database.DeviceRepo) bool { // 1. check if the device is healthy if device.DeviceStatus == pkg.DeviceStatusPassed { return false @@ -62,12 +62,16 @@ func ShouldNotify(device models.Device, smartAttrs measurements.Smart, statusThr } var failingAttributes []string + // Loop through the attributes to find the failing ones for attrId, attrData := range smartAttrs.Attributes { var status pkg.AttributeStatus = attrData.GetStatus() + // Skip over passing attributes if status == pkg.AttributeStatusPassed { continue } + // If the user only wants to consider critical attributes, we have to check + // if the not-passing attribute is critical or not if statusFilterAttributes == pkg.MetricsStatusFilterAttributesCritical { critical := false if device.IsScsi() { @@ -82,34 +86,38 @@ func ShouldNotify(device models.Device, smartAttrs measurements.Smart, statusThr } critical = thresholds.AtaMetadata[attrIdInt].Critical } + // Skip non-critical, non-passing attributes when this setting is on if !critical { continue } } + // Record any attribute that doesn't get skipped by the above two checks failingAttributes = append(failingAttributes, attrId) } + // If the user doesn't want repeated notifications when the failing value doesn't change, we need to get the last value from the db + var lastPoints []measurements.Smart + var err error if !repeatNotifications { - lastPoints, err := deviceRepo.GetSmartAttributeHistory(c, c.Param("wwn"), database.DURATION_KEY_FOREVER, 1, 1, failingAttributes) - if err == nil && len(lastPoints) > 1 { - for _, attrId := range failingAttributes { - if lastPoints[0].Attributes[attrId].GetTransformedValue() != smartAttrs.Attributes[attrId].GetTransformedValue() { - return true - } - } - return false + lastPoints, err = deviceRepo.GetSmartAttributeHistory(c, c.Param("wwn"), database.DURATION_KEY_FOREVER, 1, 1, failingAttributes) + if err == nil || len(lastPoints) < 1 { + logger.Warningln("Could not get the most recent data points from the database. This is expected to happen only if this is the very first submission of data for the device.") } - return true - } else { - for _, attr := range failingAttributes { - attrStatus := smartAttrs.Attributes[attr].GetStatus() - if pkg.AttributeStatusHas(attrStatus, requiredAttrStatus) { + } + for _, attrId := range failingAttributes { + attrStatus := smartAttrs.Attributes[attrId].GetStatus() + if pkg.AttributeStatusHas(attrStatus, requiredAttrStatus) { + if repeatNotifications { + return true + } + // This is checked again here to avoid repeating the entire for loop in the check above. + // Probably unnoticeably worse performance, but cleaner code. + if err != nil || len(lastPoints) < 1 || lastPoints[0].Attributes[attrId].GetTransformedValue() != smartAttrs.Attributes[attrId].GetTransformedValue() { return true } } } - return false } diff --git a/webapp/backend/pkg/web/handler/upload_device_metrics.go b/webapp/backend/pkg/web/handler/upload_device_metrics.go index f83107f..0814433 100644 --- a/webapp/backend/pkg/web/handler/upload_device_metrics.go +++ b/webapp/backend/pkg/web/handler/upload_device_metrics.go @@ -70,6 +70,7 @@ func UploadDeviceMetrics(c *gin.Context) { //check for error if notify.ShouldNotify( + logger, updatedDevice, smartData, pkg.MetricsStatusThreshold(appConfig.GetInt(fmt.Sprintf("%s.metrics.status_threshold", config.DB_USER_SETTINGS_SUBKEY))), From 1c193aa0430c7c7068712d6217914eb72b9a9a45 Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Tue, 23 Jan 2024 11:58:54 -0800 Subject: [PATCH 06/10] add database interface mock --- .../pkg/database/mock/mock_database.go | 258 ++++++++++++++++++ 1 file changed, 258 insertions(+) create mode 100644 webapp/backend/pkg/database/mock/mock_database.go diff --git a/webapp/backend/pkg/database/mock/mock_database.go b/webapp/backend/pkg/database/mock/mock_database.go new file mode 100644 index 0000000..c001d08 --- /dev/null +++ b/webapp/backend/pkg/database/mock/mock_database.go @@ -0,0 +1,258 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: webapp/backend/pkg/database/interface.go + +// Package mock_database is a generated GoMock package. +package mock_database + +import ( + context "context" + reflect "reflect" + + pkg "github.com/analogj/scrutiny/webapp/backend/pkg" + models "github.com/analogj/scrutiny/webapp/backend/pkg/models" + collector "github.com/analogj/scrutiny/webapp/backend/pkg/models/collector" + measurements "github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements" + gomock "github.com/golang/mock/gomock" +) + +// MockDeviceRepo is a mock of DeviceRepo interface. +type MockDeviceRepo struct { + ctrl *gomock.Controller + recorder *MockDeviceRepoMockRecorder +} + +// MockDeviceRepoMockRecorder is the mock recorder for MockDeviceRepo. +type MockDeviceRepoMockRecorder struct { + mock *MockDeviceRepo +} + +// NewMockDeviceRepo creates a new mock instance. +func NewMockDeviceRepo(ctrl *gomock.Controller) *MockDeviceRepo { + mock := &MockDeviceRepo{ctrl: ctrl} + mock.recorder = &MockDeviceRepoMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDeviceRepo) EXPECT() *MockDeviceRepoMockRecorder { + return m.recorder +} + +// Close mocks base method. +func (m *MockDeviceRepo) Close() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Close") + ret0, _ := ret[0].(error) + return ret0 +} + +// Close indicates an expected call of Close. +func (mr *MockDeviceRepoMockRecorder) Close() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockDeviceRepo)(nil).Close)) +} + +// DeleteDevice mocks base method. +func (m *MockDeviceRepo) DeleteDevice(ctx context.Context, wwn string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteDevice", ctx, wwn) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteDevice indicates an expected call of DeleteDevice. +func (mr *MockDeviceRepoMockRecorder) DeleteDevice(ctx, wwn interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteDevice", reflect.TypeOf((*MockDeviceRepo)(nil).DeleteDevice), ctx, wwn) +} + +// GetDeviceDetails mocks base method. +func (m *MockDeviceRepo) GetDeviceDetails(ctx context.Context, wwn string) (models.Device, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetDeviceDetails", ctx, wwn) + ret0, _ := ret[0].(models.Device) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetDeviceDetails indicates an expected call of GetDeviceDetails. +func (mr *MockDeviceRepoMockRecorder) GetDeviceDetails(ctx, wwn interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDeviceDetails", reflect.TypeOf((*MockDeviceRepo)(nil).GetDeviceDetails), ctx, wwn) +} + +// GetDevices mocks base method. +func (m *MockDeviceRepo) GetDevices(ctx context.Context) ([]models.Device, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetDevices", ctx) + ret0, _ := ret[0].([]models.Device) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetDevices indicates an expected call of GetDevices. +func (mr *MockDeviceRepoMockRecorder) GetDevices(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDevices", reflect.TypeOf((*MockDeviceRepo)(nil).GetDevices), ctx) +} + +// GetSmartAttributeHistory mocks base method. +func (m *MockDeviceRepo) GetSmartAttributeHistory(ctx context.Context, wwn, durationKey string, selectEntries, selectEntriesOffset int, attributes []string) ([]measurements.Smart, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSmartAttributeHistory", ctx, wwn, durationKey, selectEntries, selectEntriesOffset, attributes) + ret0, _ := ret[0].([]measurements.Smart) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetSmartAttributeHistory indicates an expected call of GetSmartAttributeHistory. +func (mr *MockDeviceRepoMockRecorder) GetSmartAttributeHistory(ctx, wwn, durationKey, selectEntries, selectEntriesOffset, attributes interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSmartAttributeHistory", reflect.TypeOf((*MockDeviceRepo)(nil).GetSmartAttributeHistory), ctx, wwn, durationKey, selectEntries, selectEntriesOffset, attributes) +} + +// GetSmartTemperatureHistory mocks base method. +func (m *MockDeviceRepo) GetSmartTemperatureHistory(ctx context.Context, durationKey string) (map[string][]measurements.SmartTemperature, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSmartTemperatureHistory", ctx, durationKey) + ret0, _ := ret[0].(map[string][]measurements.SmartTemperature) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetSmartTemperatureHistory indicates an expected call of GetSmartTemperatureHistory. +func (mr *MockDeviceRepoMockRecorder) GetSmartTemperatureHistory(ctx, durationKey interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSmartTemperatureHistory", reflect.TypeOf((*MockDeviceRepo)(nil).GetSmartTemperatureHistory), ctx, durationKey) +} + +// GetSummary mocks base method. +func (m *MockDeviceRepo) GetSummary(ctx context.Context) (map[string]*models.DeviceSummary, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSummary", ctx) + ret0, _ := ret[0].(map[string]*models.DeviceSummary) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetSummary indicates an expected call of GetSummary. +func (mr *MockDeviceRepoMockRecorder) GetSummary(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSummary", reflect.TypeOf((*MockDeviceRepo)(nil).GetSummary), ctx) +} + +// HealthCheck mocks base method. +func (m *MockDeviceRepo) HealthCheck(ctx context.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HealthCheck", ctx) + ret0, _ := ret[0].(error) + return ret0 +} + +// HealthCheck indicates an expected call of HealthCheck. +func (mr *MockDeviceRepoMockRecorder) HealthCheck(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockDeviceRepo)(nil).HealthCheck), ctx) +} + +// LoadSettings mocks base method. +func (m *MockDeviceRepo) LoadSettings(ctx context.Context) (*models.Settings, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LoadSettings", ctx) + ret0, _ := ret[0].(*models.Settings) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// LoadSettings indicates an expected call of LoadSettings. +func (mr *MockDeviceRepoMockRecorder) LoadSettings(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadSettings", reflect.TypeOf((*MockDeviceRepo)(nil).LoadSettings), ctx) +} + +// RegisterDevice mocks base method. +func (m *MockDeviceRepo) RegisterDevice(ctx context.Context, dev models.Device) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RegisterDevice", ctx, dev) + ret0, _ := ret[0].(error) + return ret0 +} + +// RegisterDevice indicates an expected call of RegisterDevice. +func (mr *MockDeviceRepoMockRecorder) RegisterDevice(ctx, dev interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegisterDevice", reflect.TypeOf((*MockDeviceRepo)(nil).RegisterDevice), ctx, dev) +} + +// SaveSettings mocks base method. +func (m *MockDeviceRepo) SaveSettings(ctx context.Context, settings models.Settings) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SaveSettings", ctx, settings) + ret0, _ := ret[0].(error) + return ret0 +} + +// SaveSettings indicates an expected call of SaveSettings. +func (mr *MockDeviceRepoMockRecorder) SaveSettings(ctx, settings interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveSettings", reflect.TypeOf((*MockDeviceRepo)(nil).SaveSettings), ctx, settings) +} + +// SaveSmartAttributes mocks base method. +func (m *MockDeviceRepo) SaveSmartAttributes(ctx context.Context, wwn string, collectorSmartData collector.SmartInfo) (measurements.Smart, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SaveSmartAttributes", ctx, wwn, collectorSmartData) + ret0, _ := ret[0].(measurements.Smart) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SaveSmartAttributes indicates an expected call of SaveSmartAttributes. +func (mr *MockDeviceRepoMockRecorder) SaveSmartAttributes(ctx, wwn, collectorSmartData interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveSmartAttributes", reflect.TypeOf((*MockDeviceRepo)(nil).SaveSmartAttributes), ctx, wwn, collectorSmartData) +} + +// SaveSmartTemperature mocks base method. +func (m *MockDeviceRepo) SaveSmartTemperature(ctx context.Context, wwn, deviceProtocol string, collectorSmartData collector.SmartInfo) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SaveSmartTemperature", ctx, wwn, deviceProtocol, collectorSmartData) + ret0, _ := ret[0].(error) + return ret0 +} + +// SaveSmartTemperature indicates an expected call of SaveSmartTemperature. +func (mr *MockDeviceRepoMockRecorder) SaveSmartTemperature(ctx, wwn, deviceProtocol, collectorSmartData interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveSmartTemperature", reflect.TypeOf((*MockDeviceRepo)(nil).SaveSmartTemperature), ctx, wwn, deviceProtocol, collectorSmartData) +} + +// UpdateDevice mocks base method. +func (m *MockDeviceRepo) UpdateDevice(ctx context.Context, wwn string, collectorSmartData collector.SmartInfo) (models.Device, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateDevice", ctx, wwn, collectorSmartData) + ret0, _ := ret[0].(models.Device) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateDevice indicates an expected call of UpdateDevice. +func (mr *MockDeviceRepoMockRecorder) UpdateDevice(ctx, wwn, collectorSmartData interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateDevice", reflect.TypeOf((*MockDeviceRepo)(nil).UpdateDevice), ctx, wwn, collectorSmartData) +} + +// UpdateDeviceStatus mocks base method. +func (m *MockDeviceRepo) UpdateDeviceStatus(ctx context.Context, wwn string, status pkg.DeviceStatus) (models.Device, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateDeviceStatus", ctx, wwn, status) + ret0, _ := ret[0].(models.Device) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateDeviceStatus indicates an expected call of UpdateDeviceStatus. +func (mr *MockDeviceRepoMockRecorder) UpdateDeviceStatus(ctx, wwn, status interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateDeviceStatus", reflect.TypeOf((*MockDeviceRepo)(nil).UpdateDeviceStatus), ctx, wwn, status) +} From 2aa242e364a7f9a96d02aef08c9049f32dbd194e Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Tue, 23 Jan 2024 12:00:01 -0800 Subject: [PATCH 07/10] update mockgen instructions --- webapp/backend/pkg/database/interface.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/webapp/backend/pkg/database/interface.go b/webapp/backend/pkg/database/interface.go index 2b34a1f..8613eae 100644 --- a/webapp/backend/pkg/database/interface.go +++ b/webapp/backend/pkg/database/interface.go @@ -9,6 +9,8 @@ import ( "github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements" ) +// Create mock using: +// mockgen -source=webapp/backend/pkg/database/interface.go -destination=webapp/backend/pkg/database/mock/mock_database.go type DeviceRepo interface { Close() error HealthCheck(ctx context.Context) error From cc889f2a2d3880639eae667eeb4cb42d5823a304 Mon Sep 17 00:00:00 2001 From: Aram Akhavan Date: Sat, 3 Feb 2024 14:33:57 -0800 Subject: [PATCH 08/10] fix notify tests --- webapp/backend/pkg/notify/notify_test.go | 59 ++++++++++++++++++------ 1 file changed, 44 insertions(+), 15 deletions(-) diff --git a/webapp/backend/pkg/notify/notify_test.go b/webapp/backend/pkg/notify/notify_test.go index c76a924..144edf3 100644 --- a/webapp/backend/pkg/notify/notify_test.go +++ b/webapp/backend/pkg/notify/notify_test.go @@ -2,12 +2,17 @@ package notify import ( "fmt" + "testing" + "time" + "github.com/analogj/scrutiny/webapp/backend/pkg" + mock_database "github.com/analogj/scrutiny/webapp/backend/pkg/database/mock" "github.com/analogj/scrutiny/webapp/backend/pkg/models" "github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements" + "github.com/gin-gonic/gin" + "github.com/golang/mock/gomock" + "github.com/sirupsen/logrus" "github.com/stretchr/testify/require" - "testing" - "time" ) func TestShouldNotify_MustSkipPassingDevices(t *testing.T) { @@ -20,22 +25,27 @@ func TestShouldNotify_MustSkipPassingDevices(t *testing.T) { statusThreshold := pkg.MetricsStatusThresholdBoth notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl) //assert - require.False(t, ShouldNotify(device, smartAttrs, statusThreshold, notifyFilterAttributes)) + require.False(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase)) } func TestShouldNotify_MetricsStatusThresholdBoth_FailingSmartDevice(t *testing.T) { t.Parallel() - //setup + //setupD device := models.Device{ DeviceStatus: pkg.DeviceStatusFailedSmart, } smartAttrs := measurements.Smart{} statusThreshold := pkg.MetricsStatusThresholdBoth notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll - + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl) //assert - require.True(t, ShouldNotify(device, smartAttrs, statusThreshold, notifyFilterAttributes)) + require.True(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase)) } func TestShouldNotify_MetricsStatusThresholdSmart_FailingSmartDevice(t *testing.T) { @@ -47,9 +57,11 @@ func TestShouldNotify_MetricsStatusThresholdSmart_FailingSmartDevice(t *testing. smartAttrs := measurements.Smart{} statusThreshold := pkg.MetricsStatusThresholdSmart notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll - + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl) //assert - require.True(t, ShouldNotify(device, smartAttrs, statusThreshold, notifyFilterAttributes)) + require.True(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase)) } func TestShouldNotify_MetricsStatusThresholdScrutiny_FailingSmartDevice(t *testing.T) { @@ -61,9 +73,11 @@ func TestShouldNotify_MetricsStatusThresholdScrutiny_FailingSmartDevice(t *testi smartAttrs := measurements.Smart{} statusThreshold := pkg.MetricsStatusThresholdScrutiny notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll - + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl) //assert - require.False(t, ShouldNotify(device, smartAttrs, statusThreshold, notifyFilterAttributes)) + require.False(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase)) } func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithCriticalAttrs(t *testing.T) { @@ -79,9 +93,12 @@ func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithCriticalAttrs(t }} statusThreshold := pkg.MetricsStatusThresholdBoth notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl) //assert - require.True(t, ShouldNotify(device, smartAttrs, statusThreshold, notifyFilterAttributes)) + require.True(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase)) } func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithMultipleCriticalAttrs(t *testing.T) { @@ -100,9 +117,12 @@ func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithMultipleCritical }} statusThreshold := pkg.MetricsStatusThresholdBoth notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl) //assert - require.True(t, ShouldNotify(device, smartAttrs, statusThreshold, notifyFilterAttributes)) + require.True(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase)) } func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithNoCriticalAttrs(t *testing.T) { @@ -118,9 +138,12 @@ func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithNoCriticalAttrs( }} statusThreshold := pkg.MetricsStatusThresholdBoth notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl) //assert - require.False(t, ShouldNotify(device, smartAttrs, statusThreshold, notifyFilterAttributes)) + require.False(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase)) } func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithNoFailingCriticalAttrs(t *testing.T) { @@ -136,9 +159,12 @@ func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithNoFailingCritica }} statusThreshold := pkg.MetricsStatusThresholdBoth notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl) //assert - require.False(t, ShouldNotify(device, smartAttrs, statusThreshold, notifyFilterAttributes)) + require.False(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase)) } func TestShouldNotify_MetricsStatusFilterAttributesCritical_MetricsStatusThresholdSmart_WithCriticalAttrsFailingScrutiny(t *testing.T) { @@ -157,9 +183,12 @@ func TestShouldNotify_MetricsStatusFilterAttributesCritical_MetricsStatusThresho }} statusThreshold := pkg.MetricsStatusThresholdSmart notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl) //assert - require.False(t, ShouldNotify(device, smartAttrs, statusThreshold, notifyFilterAttributes)) + require.False(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase)) } func TestNewPayload(t *testing.T) { From f24abf254bb9159fe0dcb89833651c5afe23d22f Mon Sep 17 00:00:00 2001 From: Aram Akhavan Date: Sun, 4 Feb 2024 11:37:48 -0800 Subject: [PATCH 09/10] Add tests for not repeating notifications --- webapp/backend/pkg/notify/notify_test.go | 67 ++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/webapp/backend/pkg/notify/notify_test.go b/webapp/backend/pkg/notify/notify_test.go index 144edf3..1c9dc11 100644 --- a/webapp/backend/pkg/notify/notify_test.go +++ b/webapp/backend/pkg/notify/notify_test.go @@ -1,11 +1,13 @@ package notify import ( + "errors" "fmt" "testing" "time" "github.com/analogj/scrutiny/webapp/backend/pkg" + "github.com/analogj/scrutiny/webapp/backend/pkg/database" mock_database "github.com/analogj/scrutiny/webapp/backend/pkg/database/mock" "github.com/analogj/scrutiny/webapp/backend/pkg/models" "github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements" @@ -190,6 +192,71 @@ func TestShouldNotify_MetricsStatusFilterAttributesCritical_MetricsStatusThresho //assert require.False(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase)) } +func TestShouldNotify_NoRepeat_DatabaseFailure(t *testing.T) { + t.Parallel() + //setup + device := models.Device{ + DeviceStatus: pkg.DeviceStatusFailedScrutiny, + } + smartAttrs := measurements.Smart{Attributes: map[string]measurements.SmartAttribute{ + "5": &measurements.SmartAtaAttribute{ + Status: pkg.AttributeStatusFailedScrutiny, + }, + }} + statusThreshold := pkg.MetricsStatusThresholdBoth + notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl) + fakeDatabase.EXPECT().GetSmartAttributeHistory(&gin.Context{}, "", database.DURATION_KEY_FOREVER, 1, 1, []string{"5"}).Return([]measurements.Smart{}, errors.New("")).Times(1) + + //assert + require.True(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, false, &gin.Context{}, fakeDatabase)) +} + +func TestShouldNotify_NoRepeat_NoDatabaseData(t *testing.T) { + t.Parallel() + //setup + device := models.Device{ + DeviceStatus: pkg.DeviceStatusFailedScrutiny, + } + smartAttrs := measurements.Smart{Attributes: map[string]measurements.SmartAttribute{ + "5": &measurements.SmartAtaAttribute{ + Status: pkg.AttributeStatusFailedScrutiny, + }, + }} + statusThreshold := pkg.MetricsStatusThresholdBoth + notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl) + fakeDatabase.EXPECT().GetSmartAttributeHistory(&gin.Context{}, "", database.DURATION_KEY_FOREVER, 1, 1, []string{"5"}).Return([]measurements.Smart{}, nil).Times(1) + + //assert + require.True(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, false, &gin.Context{}, fakeDatabase)) +} +func TestShouldNotify_NoRepeat(t *testing.T) { + t.Parallel() + //setup + device := models.Device{ + DeviceStatus: pkg.DeviceStatusFailedScrutiny, + } + smartAttrs := measurements.Smart{Attributes: map[string]measurements.SmartAttribute{ + "5": &measurements.SmartAtaAttribute{ + Status: pkg.AttributeStatusFailedScrutiny, + TransformedValue: 0, + }, + }} + statusThreshold := pkg.MetricsStatusThresholdBoth + notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl) + fakeDatabase.EXPECT().GetSmartAttributeHistory(&gin.Context{}, "", database.DURATION_KEY_FOREVER, 1, 1, []string{"5"}).Return([]measurements.Smart{smartAttrs}, nil).Times(1) + + //assert + require.False(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, false, &gin.Context{}, fakeDatabase)) +} func TestNewPayload(t *testing.T) { t.Parallel() From 09f4b34bf0061f61422b04a07b4f1e7211873800 Mon Sep 17 00:00:00 2001 From: Aram Akhavan Date: Sun, 4 Feb 2024 11:52:30 -0800 Subject: [PATCH 10/10] fix server test --- webapp/backend/pkg/web/server_test.go | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/webapp/backend/pkg/web/server_test.go b/webapp/backend/pkg/web/server_test.go index ac775a0..bd2407c 100644 --- a/webapp/backend/pkg/web/server_test.go +++ b/webapp/backend/pkg/web/server_test.go @@ -4,6 +4,16 @@ import ( "bytes" "encoding/json" "fmt" + "io" + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "path" + "strings" + "testing" + "time" + "github.com/analogj/scrutiny/webapp/backend/pkg" "github.com/analogj/scrutiny/webapp/backend/pkg/config" mock_config "github.com/analogj/scrutiny/webapp/backend/pkg/config/mock" @@ -14,15 +24,6 @@ import ( "github.com/sirupsen/logrus" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" - "io" - "io/ioutil" - "net/http" - "net/http/httptest" - "os" - "path" - "strings" - "testing" - "time" ) /* @@ -189,6 +190,7 @@ func (suite *ServerTestSuite) TestUploadDeviceMetricsRoute() { fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes() fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes() fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes() + fakeConfig.EXPECT().GetBool("user.metrics.repeat_notifications").Return(true).AnyTimes() fakeConfig.EXPECT().GetBool("web.influxdb.tls.insecure_skip_verify").Return(false).AnyTimes() fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes() if _, isGithubActions := os.LookupEnv("GITHUB_ACTIONS"); isGithubActions { @@ -247,6 +249,7 @@ func (suite *ServerTestSuite) TestPopulateMultiple() { fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes() fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes() fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes() + fakeConfig.EXPECT().GetBool("user.metrics.repeat_notifications").Return(true).AnyTimes() fakeConfig.EXPECT().GetBool("web.influxdb.tls.insecure_skip_verify").Return(false).AnyTimes() fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes() if _, isGithubActions := os.LookupEnv("GITHUB_ACTIONS"); isGithubActions { @@ -529,6 +532,7 @@ func (suite *ServerTestSuite) TestGetDevicesSummaryRoute_Nvme() { fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes() fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes() fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes() + fakeConfig.EXPECT().GetBool("user.metrics.repeat_notifications").Return(true).AnyTimes() fakeConfig.EXPECT().GetBool("web.influxdb.tls.insecure_skip_verify").Return(false).AnyTimes() fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes() fakeConfig.EXPECT().GetStringSlice("notify.urls").AnyTimes().Return([]string{})