From 991880f1c9c3c77c6e4f3abbb95d21ebb1804eaf Mon Sep 17 00:00:00 2001 From: Julien Riou Date: Fri, 22 Jul 2022 19:34:03 +0200 Subject: [PATCH 01/10] feat: Add NVIDIA FE (#15) Signed-off-by: Julien Riou --- README.md | 5 ++ config.go | 14 ++++ main.go | 14 ++++ parser_nvidia_fe.go | 195 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 228 insertions(+) create mode 100644 parser_nvidia_fe.go diff --git a/README.md b/README.md index 5306dae..e033722 100644 --- a/README.md +++ b/README.md @@ -157,6 +157,11 @@ Options: * `amazon_fulfilled`: include only products packaged by Amazon * `amazon_merchant`: include only products sold by Amazon * `affiliate_links`: generate affiliate links with the partner tag +* `nvidia_fe` (optional) + * `locations`: list of NVIDIA stores (ex `["es", "fr", "it"]`) + * `gpus`: list of models (ex: `["RTX 3060 Ti", "RTX 3070"]`) + * `user_agent`: user agent to simulate a real web browser (ex: `Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0`) + * `timeout`: maximum time before closing the request (optional) * `twitter` (optional): * `consumer_key`: API key of your Twitter application * `consumer_secret`: API secret of your Twitter application diff --git a/config.go b/config.go index d71468f..7c9c279 100644 --- a/config.go +++ b/config.go @@ -13,6 +13,7 @@ type Config struct { TelegramConfig `json:"telegram"` APIConfig `json:"api"` AmazonConfig `json:"amazon"` + NvidiaFEConfig `json:"nvidia_fe"` URLs []string `json:"urls"` IncludeRegex string `json:"include_regex"` ExcludeRegex string `json:"exclude_regex"` @@ -66,6 +67,14 @@ type AmazonConfig struct { AffiliateLinks bool `json:"affiliate_links"` } +// NvidiaFEConfig to store NVIDIA Founders Edition configuration +type NvidiaFEConfig struct { + Locations []string `json:"locations"` + GPUs []string `json:"gpus"` + UserAgent string `json:"user_agent"` + Timeout int `json:"timeout"` +} + // PriceRange to store rules to filter products with price outside of the range type PriceRange struct { Model string `json:"model"` @@ -118,6 +127,11 @@ func (c *Config) HasDatabase() bool { return c.DatabaseConfig.Type != "" && c.DatabaseConfig.DSN != "" } +// HasNvidiaFE returns true when NVIDIA FE has been configured +func (c *Config) HasNvidiaFE() bool { + return len(c.NvidiaFEConfig.Locations) > 0 && len(c.NvidiaFEConfig.GPUs) > 0 +} + // HasAmazon returns true when Amazon has been configured func (c *Config) HasAmazon() bool { var hasKeys, hasSearches, hasMarketplaces bool diff --git a/main.go b/main.go index 0709b2d..162696e 100644 --- a/main.go +++ b/main.go @@ -216,6 +216,20 @@ func main() { } } + if config.HasNvidiaFE() { + // create a parser for all locations + for _, location := range config.NvidiaFEConfig.Locations { + parser, err := NewNvidiaFRParser(location, config.NvidiaFEConfig.GPUs, config.NvidiaFEConfig.UserAgent, config.NvidiaFEConfig.Timeout) + if err != nil { + log.Warnf("could not create NVIDIA FE parser for location %s: %s", location, err) + continue + } + + parsers = append(parsers, parser) + log.Debugf("parser %s registered", parser) + } + } + // parse asynchronously var wg sync.WaitGroup jobsCount := 0 diff --git a/parser_nvidia_fe.go b/parser_nvidia_fe.go new file mode 100644 index 0000000..df0034c --- /dev/null +++ b/parser_nvidia_fe.go @@ -0,0 +1,195 @@ +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "strconv" + "time" + + log "github.com/sirupsen/logrus" +) + +// Mapping between GPU model and NVIDIA SKU on the API +var nvidiaSKUs = map[string]string{ + "RTX 3060 Ti": "NVGFT060T", + "RTX 3070": "NVGFT070", + "RTX 3070 Ti": "NVGFT070T", + "RTX 3080": "NVGFT080", + "RTX 3080 Ti": "NVGFT080T", + "RTX 3090": "NVGFT090", + "RTX 3090 Ti": "NVGFT090T", +} + +// Mapping between location and currency +var nvidiaCurrencies = map[string]string{ + "es": "EUR", + "fr": "EUR", + "it": "EUR", +} + +var supportedNvidiaGpus = []string{"RTX 3060 Ti", "RTX 3070", "RTX 3070 Ti", "RTX 3080", "RTX 3080 Ti", "RTX 3090", "RTX 3090 Ti"} +var supportedNvidiaFELocations = []string{"es", "fr", "it"} + +type NvidiaFEParser struct { + location string + gpus []string + userAgent string + client *http.Client +} + +// NewNvidiaFRParser creates a parser for NVIDIA Founders Edition website for a specific location +// Takes a location (ex: fr) and GPU (ex: RTX 3070, RTX 3090) +func NewNvidiaFRParser(location string, gpus []string, userAgent string, timeout int) (*NvidiaFEParser, error) { + // Check supported locales + if !ContainsString(supportedNvidiaFELocations, location) { + return nil, fmt.Errorf("location %s not supported (expect one of %s", location, supportedNvidiaFELocations) + } + + // Check supported GPU list + for _, gpu := range gpus { + if !ContainsString(supportedNvidiaGpus, gpu) { + return nil, fmt.Errorf("GPU %s not supported, expected one of %s", gpu, supportedNvidiaGpus) + } + } + + // Empty user agent will return an error on the NVIDIA API + // It's probably a security measure to avoid third-party robots + if userAgent == "" { + return nil, fmt.Errorf("user agent required (use the same one as your web browser)") + } + + client := &http.Client{Timeout: time.Duration(timeout) * time.Second} + + return &NvidiaFEParser{ + location: location, + gpus: gpus, + userAgent: userAgent, + client: client, + }, nil +} + +// ShopName returns a nice name for NVIDIA Founders Edition website +// Implements the Parser interface +func (p *NvidiaFEParser) ShopName() (string, error) { + return fmt.Sprintf("www.nvidia.com/%s-%s/shop/", p.location, p.location), nil +} + +// String to print NvidiaFEParser +// Implements the Parser interface +func (p *NvidiaFEParser) String() string { + return fmt.Sprintf("NvidiaFEParser<%s>", p.location) +} + +// NvidiaFEResponse to store NVIDIA API response +type NvidiaFEResponse struct { + Success bool `json:"success"` + ListMap []struct { + IsActive string `json:"is_active"` + Price string `json:"price"` + } +} + +// Parse NVIDIA store API to return list of products +// Implements Parser interface +func (p *NvidiaFEParser) Parse() ([]*Product, error) { + var products []*Product + for _, gpu := range p.gpus { + sku := nvidiaSKUs[gpu] + apiURL := fmt.Sprintf("https://api.store.nvidia.com/partner/v1/feinventory?status=1&skus=%s&locale=%s-%s", sku, p.location, p.location) + + req, err := http.NewRequest(http.MethodGet, apiURL, nil) + if err != nil { + return nil, fmt.Errorf("failed to create new request: %s", err) + } + + req.Header.Set("User-Agent", p.userAgent) + req.Header.Set("Accept", "application/json") + + log.Debugf("requesting NVIDIA API: %s", req) + + res, err := p.client.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to request NVIDIA API: %s", err) + } + + if res.Body != nil { + defer res.Body.Close() + } + + body, err := ioutil.ReadAll(res.Body) + if err != nil { + return nil, fmt.Errorf("failed to read body: %s", err) + } + + if res.StatusCode != http.StatusOK { + log.Debugf("%s", body) + return nil, fmt.Errorf("NVIDIA API returned %d", res.StatusCode) + } + + response := NvidiaFEResponse{} + err = json.Unmarshal(body, &response) + if err != nil { + return nil, fmt.Errorf("failed to parse JSON response: %s", err) + } + + if !response.Success { + return nil, fmt.Errorf("NVIDIA API returned an applicative failure for GPU %s and location %s", gpu, p.location) + } + + for _, element := range response.ListMap { + + var product = &Product{ + Name: gpu, + PriceCurrency: nvidiaCurrencies[p.location], + } + + available, err := strconv.ParseBool(element.IsActive) + if err != nil { + return nil, fmt.Errorf("failed to parse bool from response: %s", err) + } + product.Available = available + + productPrice, err := strconv.ParseFloat(element.Price, 64) + if err != nil { + return nil, fmt.Errorf("failed to parse float from response: %s", err) + } + product.Price = productPrice + + productURL, err := createNvidiaFEProductURL(p.location, gpu) + if err != nil { + return nil, fmt.Errorf("failed to create product URL: %s", err) + } + product.URL = productURL + products = append(products, product) + } + + } + return products, nil +} + +// Create the product URL +// Ex: https://store.nvidia.com/fr-fr/geforce/store/gpu/?page=1&limit=100&locale=fr-fr&category=GPU&gpu=RTX%203080&manufacturer=NVIDIA +func createNvidiaFEProductURL(location string, gpu string) (string, error) { + locale := fmt.Sprintf("%s-%s", location, location) + + productURL, err := url.Parse("https://store.nvidia.com") + if err != nil { + return "", err + } + + productURL.Path += fmt.Sprintf("%s/geforce/store/gpu/", locale) + + params := url.Values{} + params.Add("page", "1") + params.Add("limit", "100") + params.Add("locale", locale) + params.Add("category", "GPU") + params.Add("gpu", gpu) + params.Add("manufacturer", "NVIDIA") + + productURL.RawQuery = params.Encode() + return productURL.String(), nil +} From 817c4e0f6e25942110cee4c029c12402fe0bb59e Mon Sep 17 00:00:00 2001 From: Julien Riou Date: Fri, 22 Jul 2022 19:35:12 +0200 Subject: [PATCH 02/10] chore: Release 0.7.0 Signed-off-by: Julien Riou --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index ee6cdce..bcaffe1 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.6.1 +0.7.0 \ No newline at end of file From cb53106e1f40eabaa8b7dd29c04a04b853798fab Mon Sep 17 00:00:00 2001 From: Julien Riou Date: Sat, 23 Jul 2022 08:56:07 +0200 Subject: [PATCH 03/10] chore: Rename NVIDIA FE shop Signed-off-by: Julien Riou --- parser_nvidia_fe.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/parser_nvidia_fe.go b/parser_nvidia_fe.go index df0034c..1371eda 100644 --- a/parser_nvidia_fe.go +++ b/parser_nvidia_fe.go @@ -74,7 +74,7 @@ func NewNvidiaFRParser(location string, gpus []string, userAgent string, timeout // ShopName returns a nice name for NVIDIA Founders Edition website // Implements the Parser interface func (p *NvidiaFEParser) ShopName() (string, error) { - return fmt.Sprintf("www.nvidia.com/%s-%s/shop/", p.location, p.location), nil + return fmt.Sprintf("nvidia.com/%s-%s/shop", p.location, p.location), nil } // String to print NvidiaFEParser From 3b392700d3a3c909b7619fbeb4c5d9e56308cd6c Mon Sep 17 00:00:00 2001 From: Julien Riou Date: Wed, 31 Aug 2022 18:40:52 +0200 Subject: [PATCH 04/10] feat: Add -monitor option Signed-off-by: Julien Riou --- .gitignore | 1 + README.md | 1 + main.go | 8 ++++ monitoring.go | 118 ++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 128 insertions(+) create mode 100644 monitoring.go diff --git a/.gitignore b/.gitignore index bed624e..ffd6401 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ restockbot.pid ferret.log shop.fql *.bak +venv diff --git a/README.md b/README.md index e033722..353f5d7 100644 --- a/README.md +++ b/README.md @@ -203,6 +203,7 @@ docker run -it --name restockbot --rm --link chromium:chromium -v $(pwd):/root/ There are two modes: * **default**: without special argument, the bot parses websites and manage its own database * **API**: using the `-api` argument, the bot starts the HTTP API to expose data from the database +* **monitor**: using the `-monitor` (optionaly with `-monitor-warning-timeout` and `-monitor-critical-timeout` arguments), the bot checks for last execution times per shop to return a Nagios compatible output ## How to contribute diff --git a/main.go b/main.go index 162696e..ccff985 100644 --- a/main.go +++ b/main.go @@ -53,6 +53,9 @@ func main() { pidWaitTimeout := flag.Int("pid-wait-timeout", 0, "Seconds to wait before giving up when another instance is running") retention := flag.Int("retention", 0, "Automatically remove products from the database with this number of days old (disabled by default)") api := flag.Bool("api", false, "Start the HTTP API") + monitor := flag.Bool("monitor", false, "Perform health check with Nagios output") + warningTimeout := flag.Int("monitor-warning-timeout", 300, "Raise a warning alert when the last execution time has reached this number of seconds (see -monitor)") + criticalTimeout := flag.Int("monitor-critical-timeout", 600, "Raise a critical alert when the last execution time has reached this number of seconds (see -monitor)") flag.Parse() @@ -138,6 +141,11 @@ func main() { } } + // start monitoring + if *monitor { + os.Exit(Monitor(db, *warningTimeout, *criticalTimeout)) + } + // start the api if *api { log.Fatal(StartAPI(db, config.APIConfig)) diff --git a/monitoring.go b/monitoring.go new file mode 100644 index 0000000..014ad5d --- /dev/null +++ b/monitoring.go @@ -0,0 +1,118 @@ +package main + +import ( + "fmt" + "strings" + "time" + + log "github.com/sirupsen/logrus" + "gorm.io/gorm" +) + +const ( + // NagiosOk return the Nagios OK code (see https://nagios-plugins.org/doc/guidelines.html#AEN78) + NagiosOk = 0 + // NagiosWarning return the Nagios WARNING code (see https://nagios-plugins.org/doc/guidelines.html#AEN78) + NagiosWarning = 1 + // NagiosCritical return the Nagios CRITICAL code (see https://nagios-plugins.org/doc/guidelines.html#AEN78) + NagiosCritical = 2 + // NagiosUnknown return the Nagios UNKNOWN code (see https://nagios-plugins.org/doc/guidelines.html#AEN78) + NagiosUnknown = 3 +) + +// MonitoringResult to store result of Nagios checks +type MonitoringResult struct { + ShopName string + UpdatedAt time.Time + ReturnCode int +} + +// String to print a MonitoringResult nicely +func (m MonitoringResult) String() string { + diff := time.Now().Sub(m.UpdatedAt) + + var wording string + if diff.Seconds() > 0 { + wording = "seconds" + } else { + wording = "second" + } + + return fmt.Sprintf("%s (%d %s ago)", m.ShopName, diff, wording) +} + +// FormatMonitoringResults to print a list of MonitoringResult nicely +func FormatMonitoringResults(results []MonitoringResult) string { + var s []string + for _, result := range results { + s = append(s, result.String()) + } + return strings.Join(s, ", ") +} + +// Monitor will check for last execution time for each shop and return either +// a warning or critical alert when the threshold has been reached +func Monitor(db *gorm.DB, warningTimeout int, criticalTimeout int) (rc int) { + + // Find date and time thresholds + warningTime := time.Now().Add(-time.Duration(warningTimeout) * time.Second) + criticalTime := time.Now().Add(-time.Duration(criticalTimeout) * time.Second) + + // Map to sort monitoring result by status code + resultMap := make(map[int][]MonitoringResult) + + // List shops + var shops []Shop + trx := db.Find(&shops) + if trx.Error != nil { + fmt.Printf("%s\n", trx.Error) + return NagiosUnknown + } + + for _, shop := range shops { + // Fetch last execution time + var product Product + trx := db.Where(Product{ShopID: shop.ID}).Order("updated_at asc").First(&product) + if trx.Error == gorm.ErrRecordNotFound { + fmt.Printf("%s\n", fmt.Errorf("No product found for shop %s", shop.Name)) + return NagiosCritical + } + if trx.Error != nil { + fmt.Printf("%s\n", trx.Error) + return NagiosUnknown + } + + // Compare to thresholds and add to result map + result := MonitoringResult{ShopName: shop.Name, UpdatedAt: product.UpdatedAt, ReturnCode: NAGIOS_OK} + if product.UpdatedAt.Before(criticalTime) { + log.Infof("%s has been updated at %s (before time of %s) (crit)", shop.Name, product.UpdatedAt, criticalTime) + result.ReturnCode = NagiosCritical + } else if product.UpdatedAt.Before(warningTime) { + log.Infof("%s has been updated at %s (before time of %s) (warn)", shop.Name, product.UpdatedAt, warningTime) + result.ReturnCode = NagiosWarning + } else { + log.Infof("%s has been updated at %s (after %s) (ok)", shop.Name, product.UpdatedAt, warningTime) + } + resultMap[result.ReturnCode] = append(resultMap[result.ReturnCode], result) + } + + var message, prefix string + + if len(resultMap[NagiosWarning]) > 0 { + rc = NagiosWarning + prefix = "WARN" + message = FormatMonitoringResults(resultMap[NagiosWarning]) + } else if len(resultMap[NagiosCritical]) > 0 { + rc = NagiosCritical + prefix = "CRIT" + message = FormatMonitoringResults(resultMap[NagiosCritical]) + } else { + rc = NagiosOK + prefix = "OK" + message = "All shops have been updated recently" + } + + // Print output + fmt.Printf("%s - %s\n", prefix, message) + return rc +} From 94eb400a28253c1f51ddb29ead382693c3db35c7 Mon Sep 17 00:00:00 2001 From: Julien Riou Date: Thu, 1 Sep 2022 07:03:33 +0200 Subject: [PATCH 05/10] doc: Add comment on NvidiaFEParser (go-lint) Signed-off-by: Julien Riou --- parser_nvidia_fe.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/parser_nvidia_fe.go b/parser_nvidia_fe.go index 1371eda..67e212b 100644 --- a/parser_nvidia_fe.go +++ b/parser_nvidia_fe.go @@ -33,6 +33,8 @@ var nvidiaCurrencies = map[string]string{ var supportedNvidiaGpus = []string{"RTX 3060 Ti", "RTX 3070", "RTX 3070 Ti", "RTX 3080", "RTX 3080 Ti", "RTX 3090", "RTX 3090 Ti"} var supportedNvidiaFELocations = []string{"es", "fr", "it"} +// NvidiaFEParser to parse NVIDIA Founder Edition website parser +// Implements the Parser interface type NvidiaFEParser struct { location string gpus []string From 0c819631314661909e3e139541c4344cfe00a87d Mon Sep 17 00:00:00 2001 From: Julien Riou Date: Thu, 1 Sep 2022 07:07:13 +0200 Subject: [PATCH 06/10] fix: Make monitoring build Signed-off-by: Julien Riou --- monitoring.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monitoring.go b/monitoring.go index 014ad5d..a59a62e 100644 --- a/monitoring.go +++ b/monitoring.go @@ -83,7 +83,7 @@ func Monitor(db *gorm.DB, warningTimeout int, criticalTimeout int) (rc int) { } // Compare to thresholds and add to result map - result := MonitoringResult{ShopName: shop.Name, UpdatedAt: product.UpdatedAt, ReturnCode: NAGIOS_OK} + result := MonitoringResult{ShopName: shop.Name, UpdatedAt: product.UpdatedAt, ReturnCode: NagiosOk} if product.UpdatedAt.Before(criticalTime) { log.Infof("%s has been updated at %s (before time of %s) (crit)", shop.Name, product.UpdatedAt, criticalTime) result.ReturnCode = NagiosCritical @@ -107,7 +107,7 @@ func Monitor(db *gorm.DB, warningTimeout int, criticalTimeout int) (rc int) { prefix = "CRIT" message = FormatMonitoringResults(resultMap[NagiosCritical]) } else { - rc = NagiosOK + rc = NagiosOk prefix = "OK" message = "All shops have been updated recently" } From e7c18d048e1f367bc31aaca675b6b1ac0a2c3d16 Mon Sep 17 00:00:00 2001 From: Julien Riou Date: Fri, 2 Sep 2022 08:49:19 +0200 Subject: [PATCH 07/10] chore: Disable low-level database error messages Signed-off-by: Julien Riou --- db.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/db.go b/db.go index ba14602..9d0fb7d 100644 --- a/db.go +++ b/db.go @@ -5,21 +5,26 @@ import ( "gorm.io/driver/postgres" "gorm.io/driver/sqlite" "gorm.io/gorm" + "gorm.io/gorm/logger" ) +var gconfig = &gorm.Config{ + Logger: logger.Default.LogMode(logger.Silent), +} + // NewDatabaseFromConfig creates a database object from configuration structure func NewDatabaseFromConfig(config DatabaseConfig) (*gorm.DB, error) { switch config.Type { case "postgres": - return gorm.Open(postgres.New(postgres.Config{DSN: config.DSN}), &gorm.Config{}) + return gorm.Open(postgres.New(postgres.Config{DSN: config.DSN}), gconfig) case "mysql": - return gorm.Open(mysql.New(mysql.Config{DSN: config.DSN}), &gorm.Config{}) + return gorm.Open(mysql.New(mysql.Config{DSN: config.DSN}), gconfig) default: - return gorm.Open(sqlite.Open(config.DSN), &gorm.Config{}) + return gorm.Open(sqlite.Open(config.DSN), gconfig) } } // NewDatabaseFromFile creates a database object from path to file (passed as argument) func NewDatabaseFromFile(path string) (*gorm.DB, error) { - return gorm.Open(sqlite.Open(path), &gorm.Config{}) + return gorm.Open(sqlite.Open(path), gconfig) } From 3a8d0825a688a7d3e1e77cb38a624f6da26351ca Mon Sep 17 00:00:00 2001 From: Julien Riou Date: Fri, 2 Sep 2022 09:11:24 +0200 Subject: [PATCH 08/10] refactor(monitoring): Improve code and output - Don't return when a shop is missing - Make MonitoringResult more generic to handle messages Signed-off-by: Julien Riou --- monitoring.go | 56 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/monitoring.go b/monitoring.go index a59a62e..58f35e7 100644 --- a/monitoring.go +++ b/monitoring.go @@ -23,29 +23,34 @@ const ( // MonitoringResult to store result of Nagios checks type MonitoringResult struct { ShopName string - UpdatedAt time.Time + Message string ReturnCode int } -// String to print a MonitoringResult nicely +// String returns a string to print a MonitoringResult nicely func (m MonitoringResult) String() string { - diff := time.Now().Sub(m.UpdatedAt) + return fmt.Sprintf("%s %s (rc = %d)", m.ShopName, m.Message, m.ReturnCode) +} - var wording string - if diff.Seconds() > 0 { - wording = "seconds" - } else { - wording = "second" +// ReturnCodeString returns a string to print a ReturnCode nicely +func ReturnCodeString(rc int) string { + switch rc { + case NagiosOk: + return "OK" + case NagiosWarning: + return "WARN" + case NagiosCritical: + return "CRIT" + default: + return "UNK" } - - return fmt.Sprintf("%s (%d %s ago)", m.ShopName, diff, wording) } // FormatMonitoringResults to print a list of MonitoringResult nicely func FormatMonitoringResults(results []MonitoringResult) string { var s []string for _, result := range results { - s = append(s, result.String()) + s = append(s, fmt.Sprintf("%s %s", result.ShopName, result.Message)) } return strings.Join(s, ", ") } @@ -70,12 +75,20 @@ func Monitor(db *gorm.DB, warningTimeout int, criticalTimeout int) (rc int) { } for _, shop := range shops { + + result := MonitoringResult{ + ShopName: shop.Name, + ReturnCode: NagiosOk, + } + // Fetch last execution time var product Product trx := db.Where(Product{ShopID: shop.ID}).Order("updated_at asc").First(&product) if trx.Error == gorm.ErrRecordNotFound { - fmt.Printf("%s\n", fmt.Errorf("No product found for shop %s", shop.Name)) - return NagiosCritical + result.Message = "has not been updated" + result.ReturnCode = NagiosCritical + resultMap[NagiosCritical] = append(resultMap[result.ReturnCode], result) + continue } if trx.Error != nil { fmt.Printf("%s\n", trx.Error) @@ -83,36 +96,33 @@ func Monitor(db *gorm.DB, warningTimeout int, criticalTimeout int) (rc int) { } // Compare to thresholds and add to result map - result := MonitoringResult{ShopName: shop.Name, UpdatedAt: product.UpdatedAt, ReturnCode: NagiosOk} + diff := int(time.Now().Sub(product.UpdatedAt.Local()).Seconds()) + result.Message = fmt.Sprintf("updated %d seconds ago", diff) + if product.UpdatedAt.Before(criticalTime) { - log.Infof("%s has been updated at %s (before time of %s) (crit)", shop.Name, product.UpdatedAt, criticalTime) result.ReturnCode = NagiosCritical } else if product.UpdatedAt.Before(warningTime) { - log.Infof("%s has been updated at %s (before time of %s) (warn)", shop.Name, product.UpdatedAt, warningTime) result.ReturnCode = NagiosWarning } else { - log.Infof("%s has been updated at %s (after %s) (ok)", shop.Name, product.UpdatedAt, warningTime) } + log.Info(result) resultMap[result.ReturnCode] = append(resultMap[result.ReturnCode], result) } - var message, prefix string + var message string if len(resultMap[NagiosWarning]) > 0 { rc = NagiosWarning - prefix = "WARN" message = FormatMonitoringResults(resultMap[NagiosWarning]) } else if len(resultMap[NagiosCritical]) > 0 { rc = NagiosCritical - prefix = "CRIT" message = FormatMonitoringResults(resultMap[NagiosCritical]) } else { rc = NagiosOk - prefix = "OK" message = "All shops have been updated recently" } // Print output - fmt.Printf("%s - %s\n", prefix, message) - return rc + fmt.Printf("%s - %s\n", ReturnCodeString(rc), message) + return } From 0d5e90dd9e144198f0788248aed7f7ce53428fd6 Mon Sep 17 00:00:00 2001 From: Julien Riou Date: Fri, 2 Sep 2022 09:22:33 +0200 Subject: [PATCH 09/10] fix(monitoring): Fix product ordering Products were ordered by old update timestamp first. We need the recent timestamps first. Signed-off-by: Julien Riou --- monitoring.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monitoring.go b/monitoring.go index 58f35e7..0dba1dc 100644 --- a/monitoring.go +++ b/monitoring.go @@ -83,7 +83,7 @@ func Monitor(db *gorm.DB, warningTimeout int, criticalTimeout int) (rc int) { // Fetch last execution time var product Product - trx := db.Where(Product{ShopID: shop.ID}).Order("updated_at asc").First(&product) + trx := db.Where(Product{ShopID: shop.ID}).Order("updated_at desc").First(&product) if trx.Error == gorm.ErrRecordNotFound { result.Message = "has not been updated" result.ReturnCode = NagiosCritical From d774ee8057c774186bf5c7ba31256fc755788394 Mon Sep 17 00:00:00 2001 From: Julien Riou Date: Wed, 1 Nov 2023 09:09:41 +0100 Subject: [PATCH 10/10] chore: End of life notice Signed-off-by: Julien Riou --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 353f5d7..26ac31f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,8 @@ +# END OF LIFE NOTICE + +Due to lack of time to do the maintenance, this repository is now archived. +Don't hesitate to fork it! + # RestockBot Year 2020 has been quite hard for hardware supply. Graphics cards are out of stock everywhere. Nobody can grab the new generation (AMD RX 6000 series, NVIDIA GeForce RTX 3000 series). Even older generations are hard to find. `RestockBot` is a bot that crawl retailers websites and notify when a product is available.