├── logo.png ├── go.mod ├── .gitignore ├── test.env ├── go.sum ├── .github └── workflows │ └── gotest.yml ├── CONTRIBUTING.md ├── LICENSE ├── alert.go ├── email.go ├── README.md ├── throttler.go ├── ms_teams.go ├── alert_test.go └── throttler_test.go /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rakutentech/go-alertnotification/HEAD/logo.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/rakutentech/go-alertnotification/v2 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/GitbookIO/diskache v0.0.0-20161028144708-bfb81bf58cb1 7 | github.com/joho/godotenv v1.5.1 8 | ) 9 | 10 | require github.com/GitbookIO/syncgroup v0.0.0-20200915204659-4f0b2961ab10 // indirect 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | # diskcache directory 14 | cache/* 15 | vendor/* 16 | 17 | -------------------------------------------------------------------------------- /test.env: -------------------------------------------------------------------------------- 1 | SMTP_HOST=localhost 2 | SMTP_PORT=25 3 | EMAIL_SENDER=test@example.com 4 | EMAIL_RECEIVERS=receiver.test@exmaple.com 5 | EMAIL_ALERT_ENABLED=true 6 | MS_TEAMS_ALERT_ENABLED= 7 | 8 | MS_TEAMS_CARD_SUBJECT=test subject 9 | ALERT_THEME_COLOR=ff5864 10 | ALERT_CARD_SUBJECT=Errror card 11 | MS_TEAMS_CARD_SUBJECT=teams card 12 | APP_ENV=local 13 | APP_NAME=golang 14 | MS_TEAMS_WEBHOOK=Teams webhook -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/GitbookIO/diskache v0.0.0-20161028144708-bfb81bf58cb1 h1:1ui53h0HCYjyrlza+yY+sQsulJdHdYL/xdVWIH3UsyE= 2 | github.com/GitbookIO/diskache v0.0.0-20161028144708-bfb81bf58cb1/go.mod h1:TTHndD25/UJVOyBl/vOq2g5RIg4bidGlmtzb+4Zr+Nw= 3 | github.com/GitbookIO/syncgroup v0.0.0-20200915204659-4f0b2961ab10 h1:G9KsBi5RxXROehPm+TSvTrFXShD613GLKrv9ctY1hFE= 4 | github.com/GitbookIO/syncgroup v0.0.0-20200915204659-4f0b2961ab10/go.mod h1:QEGLOlzj5q/UbkPM0viAulgbdRUpsU3/6HVA9YUA9BU= 5 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 6 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 7 | -------------------------------------------------------------------------------- /.github/workflows/gotest.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: Test 3 | jobs: 4 | test: 5 | strategy: 6 | matrix: 7 | go-version: [mod, dev-latest] 8 | os: [ubuntu-latest] 9 | runs-on: ${{ matrix.os }} 10 | env: 11 | GO111MODULE: on 12 | steps: 13 | - name: Cancel Previous Runs 14 | uses: styfle/cancel-workflow-action@0.9.1 15 | with: 16 | access_token: ${{ github.token }} 17 | - uses: actions/checkout@v2 18 | - uses: kevincobain2000/action-gobrew@v2 19 | with: 20 | go-version: ${{ matrix.go }} 21 | - name: Test 22 | run: go test -v ./... 23 | - name: Vet 24 | run: go vet -v ./... 25 | - name: Run Gosec Security Scanner 26 | uses: securego/gosec@master 27 | with: 28 | args: ./... -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Found a Bug? 4 | 5 | If you find a bug in the source code, you can help us by submitting an issue to our GitHub Repository. Even better, you can submit a Pull Request with a fix. 6 | 7 | ## Coding Guidelines 8 | 9 | - All features or bug fixes must be tested by one or more unit tests/specs 10 | - All public API methods must be documented and potentially also described in the user guide. 11 | 12 | 13 | ## Pull Requests 14 | 15 | 1. Fork the project 16 | 2. Implement feature/fix bug & add test cases 17 | 3. Ensure test cases & static analysis runs succesfully 18 | 4. Submit a pull request to develop branch 19 | 20 | **NOTE:** When submitting a pull request, *please make sure to target the `develop` branch*, so that your changes are up-to-date and easy to integrate with the most recent work on the buildpack. Thanks! 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Rakuten, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /alert.go: -------------------------------------------------------------------------------- 1 | package alertnotification 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | // Alert struct for specify the ignoring error and the occuring error 9 | type Alert struct { 10 | Error error 11 | DoNotAlertErrors []error 12 | Expandos *Expandos 13 | } 14 | 15 | // NewAlert creates Alert struct instance 16 | func NewAlert(err error, doNotAlertErrors []error) Alert { 17 | a := Alert{ 18 | Error: err, 19 | DoNotAlertErrors: doNotAlertErrors, 20 | Expandos: nil, 21 | } 22 | return a 23 | } 24 | 25 | // NewAlertWithExpandos creates Alert struct instance with expandos 26 | func NewAlertWithExpandos(err error, doNotAlertErrors []error, expandos *Expandos) Alert { 27 | a := Alert{ 28 | Error: err, 29 | DoNotAlertErrors: doNotAlertErrors, 30 | Expandos: expandos, 31 | } 32 | return a 33 | } 34 | 35 | // Expandos struct for body and subject 36 | type Expandos struct { 37 | EmailBody string 38 | EmailSubject string 39 | MsTeamsAlertCardSubject string 40 | MsTeamsCardSubject string 41 | MsTeamsError string 42 | } 43 | 44 | // AlertNotification is interface that all send notification function satify including send email 45 | type AlertNotification interface { 46 | Send() error 47 | } 48 | 49 | // DoSendNotification is to send the alert to the specified implemenation of the AlertNoticication interface 50 | func DoSendNotification(alert AlertNotification) error { 51 | return alert.Send() 52 | } 53 | 54 | // Notify send and do throttling when error occur 55 | func (a *Alert) Notify() (err error) { 56 | if a.shouldAlert() { 57 | err := a.dispatch() 58 | fmt.Println(err) 59 | if err != nil { 60 | return err 61 | } 62 | } 63 | return 64 | } 65 | 66 | // Dispatch sends all notification to all registered chanel 67 | func (a *Alert) dispatch() (err error) { 68 | if shouldMail() { 69 | fmt.Println("Send mail....") 70 | e := NewEmailConfig(a.Error, a.Expandos) 71 | err := e.Send() 72 | if err != nil { 73 | return err 74 | } 75 | } 76 | 77 | if shouldMsTeams() { 78 | fmt.Println("SendTeams") 79 | m := NewMsTeam(a.Error, a.Expandos) 80 | err := m.Send() 81 | if err != nil { 82 | return err 83 | } 84 | } 85 | return 86 | } 87 | 88 | func (a *Alert) shouldAlert() bool { 89 | if !a.isThrottlingEnabled() { 90 | //Always alert when throttling is disabled. 91 | return true 92 | } 93 | 94 | if a.isDoNotAlert() { 95 | return false 96 | } 97 | t := NewThrottler() 98 | return !t.IsThrottledOrGraced(a.Error) 99 | } 100 | 101 | func (a *Alert) isDoNotAlert() bool { 102 | for _, e := range a.DoNotAlertErrors { 103 | if e.Error() == a.Error.Error() { 104 | return true 105 | } 106 | } 107 | return false 108 | } 109 | 110 | func shouldMsTeams() bool { 111 | return os.Getenv("MS_TEAMS_ALERT_ENABLED") == "true" 112 | } 113 | 114 | func shouldMail() bool { 115 | return os.Getenv("EMAIL_ALERT_ENABLED") == "true" 116 | } 117 | 118 | func (a *Alert) isThrottlingEnabled() bool { 119 | return os.Getenv("THROTTLE_ENABLED") != "false" 120 | } 121 | 122 | // RemoveCurrentThrotting remove all current throttlings. 123 | func (a *Alert) RemoveCurrentThrotting() error { 124 | t := NewThrottler() 125 | return t.CleanThrottlingCache() 126 | } 127 | -------------------------------------------------------------------------------- /email.go: -------------------------------------------------------------------------------- 1 | package alertnotification 2 | 3 | import ( 4 | "encoding/base64" 5 | "errors" 6 | "fmt" 7 | "net/smtp" 8 | "os" 9 | "strings" 10 | ) 11 | 12 | // EmailConfig is email setting struct 13 | type EmailConfig struct { 14 | Username string 15 | Password string 16 | Host string 17 | Port string 18 | Sender string 19 | EnvelopeFrom string 20 | Receivers []string // Can use comma for multiple email 21 | Subject string 22 | ErrorObj error 23 | Expandos *Expandos // can modify mail subject and content on demand 24 | } 25 | 26 | func getReceivers() []string { 27 | delimeter := "," 28 | receivers := os.Getenv("EMAIL_RECEIVERS") 29 | if len(receivers) == 0 { 30 | return nil 31 | } 32 | return strings.Split(receivers, delimeter) 33 | } 34 | 35 | // NewEmailConfig create new EmailConfig struct 36 | func NewEmailConfig(err error, expandos *Expandos) EmailConfig { 37 | config := EmailConfig{ 38 | Username: os.Getenv("EMAIL_USERNAME"), 39 | Password: os.Getenv("EMAIL_PASSWORD"), 40 | Host: os.Getenv("SMTP_HOST"), 41 | Port: os.Getenv("SMTP_PORT"), 42 | Sender: os.Getenv("EMAIL_SENDER"), 43 | EnvelopeFrom: os.Getenv("EMAIL_ENVELOPE_FROM"), 44 | Subject: os.Getenv("EMAIL_SUBJECT"), 45 | Receivers: getReceivers(), 46 | ErrorObj: err, 47 | Expandos: expandos, 48 | } 49 | if len(strings.TrimSpace(config.EnvelopeFrom)) == 0 { 50 | config.EnvelopeFrom = config.Sender 51 | } 52 | return config 53 | } 54 | 55 | // Send Alert email 56 | func (ec *EmailConfig) Send() error { 57 | fmt.Println("sending email ....") 58 | var err error 59 | if ec.Receivers == nil { 60 | return errors.New("notification receivers are empty") 61 | } 62 | r := strings.NewReplacer("\r\n", "", "\r", "", "\n", "", "%0a", "", "%0d", "") 63 | 64 | messageDetail := "Error: \r\n" + fmt.Sprintf("%+v", ec.ErrorObj) 65 | 66 | // update body and subject dynamically 67 | if ec.Expandos != nil { 68 | if ec.Expandos.EmailBody != "" { 69 | messageDetail = ec.Expandos.EmailBody 70 | } 71 | if ec.Expandos.EmailSubject != "" { 72 | ec.Subject = ec.Expandos.EmailSubject 73 | } 74 | } 75 | 76 | message := "To: " + strings.Join(ec.Receivers, ", ") + "\r\n" + 77 | "From: " + ec.Sender + "\r\n" + 78 | "Subject: " + ec.Subject + "\r\n" + 79 | "Content-Type: text/html; charset=\"UTF-8\"\r\n" + 80 | "Content-Transfer-Encoding: base64\r\n" + 81 | "\r\n" + base64.StdEncoding.EncodeToString([]byte(messageDetail)) 82 | 83 | if len(strings.TrimSpace(ec.Username)) != 0 { 84 | stmpAuth := smtp.PlainAuth("", ec.Username, ec.Password, ec.Host) 85 | 86 | err = smtp.SendMail( 87 | ec.Host+":"+ec.Port, 88 | stmpAuth, 89 | ec.EnvelopeFrom, 90 | ec.Receivers, 91 | []byte(message), 92 | ) 93 | return err 94 | } 95 | fmt.Println("Send with localhost. ......") 96 | conn, err := smtp.Dial(ec.Host + ":" + ec.Port) 97 | if err != nil { 98 | return err 99 | } 100 | 101 | defer conn.Close() 102 | if err = conn.Mail(r.Replace(ec.EnvelopeFrom)); err != nil { 103 | return err 104 | } 105 | // format receiver email 106 | for i := range ec.Receivers { 107 | ec.Receivers[i] = r.Replace(ec.Receivers[i]) 108 | if err = conn.Rcpt(ec.Receivers[i]); err != nil { 109 | return err 110 | } 111 | } 112 | 113 | w, err := conn.Data() 114 | if err != nil { 115 | return err 116 | } 117 | _, err = w.Write([]byte(message)) 118 | if err != nil { 119 | return err 120 | } 121 | err = w.Close() 122 | if err != nil { 123 | return err 124 | } 125 | return conn.Quit() 126 | 127 | } 128 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | go-alertnotification 4 | 5 |

6 | 7 |

Send Alert Notifications for Go Errors.
Notify when new error arrives.

8 | 9 |

10 | Throttle notifications to avoid overwhelming your inbox. 11 |
12 | Supports multiple Emails, MS Teams and proxy support. 13 |

14 | 15 | ## Usage 16 | 17 | ```bash 18 | go install github.com/rakutentech/go-alertnotification@latest 19 | ``` 20 | 21 | ## Configurations 22 | 23 | This package use golang env variables as settings. 24 | 25 | ### General Configs 26 | 27 | 28 | | Env Variable | default | Description | 29 | | :----------- | :------ | :------------------------------------------------------------ | 30 | | APP_ENV | | application environment to be appeared in email/teams message | 31 | | APP_NAME | | application name to be appeared in email/teams message | 32 | 33 | 34 | ### Email Configs 35 | 36 | | Env Variable | default | Description | 37 | | :------------------ | :------ | :------------------------------------------------------------------------------ | 38 | | **EMAIL_SENDER** | | **required** sender email address | 39 | | **EMAIL_RECEIVERS** | | **required** receiver email addresses. Eg. `test1@gmail.com`,`test2@gmail.com` | 40 | | EMAIL_ALERT_ENABLED | false | change to "true" to enable | 41 | | SMTP_HOST | | SMTP server hostname | 42 | | SMTP_PORT | | SMTP server port | 43 | | EMAIL_USERNAME | | SMTP username | 44 | | EMAIL_PASSWORD | | SMTP password | 45 | 46 | ### Ms Teams Configs 47 | 48 | | Env Variable | default | Description | 49 | | :--------------------- | :------ | :----------------------------- | 50 | | **MS_TEAMS_WEBHOOK** | | **required** Ms Teams webhook. | 51 | | MS_TEAMS_ALERT_ENABLED | false | change to "true" to enable | 52 | | MS_TEAMS_CARD_SUBJECT | | MS teams card subject | 53 | | ALERT_CARD_SUBJECT | | Alert MessageCard subject | 54 | | ALERT_THEME_COLOR | | Themes color | 55 | | MS_TEAMS_PROXY_URL | | Work behind corporate proxy | 56 | 57 | ### Throttling Configs 58 | 59 | | Env Variable | default | Explanation | 60 | | :--------------------- | :------------------------------------------- | :----------------------------- | 61 | | THROTTLE_DURATION | 7 | throttling duration in minutes | 62 | | THROTTLE_GRACE_SECONDS | 0 | throttling grace in seconds | 63 | | THROTTLE_DISKCACHE_DIR | `/tmp/cache/{APP_NAME}_throttler_disk_cache` | disk location for throttling | 64 | | THROTTLE_ENABLED | true | Disable all together | 65 | 66 | ## Usage 67 | 68 | ### Simple 69 | 70 | ```go 71 | //import 72 | import n "github.com/rakutentech/go-alertnotification" 73 | err := errors.New("Alert me") 74 | ignoringErrs := []error{errors.New("Ignore 001"), errors.New("Ignore 002")}; 75 | 76 | //Create New Alert 77 | alert := n.NewAlert(err, ignoringErrs) 78 | //Send notification 79 | alert.Notify() 80 | ``` 81 | 82 | ### With customized fields 83 | 84 | ```go 85 | import n "github.com/rakutentech/go-alertnotification" 86 | 87 | //Create expandos, can keep the field value as configured by removing that field from expandos 88 | expandos := &n.Expandos{ 89 | EmailBody: "This is the customized email body", 90 | EmailSubject: "This is the customized email subject", 91 | MsTeamsCardSubject: "This is the customized MS Teams card summary", 92 | MsTeamsAlertCardSubject: "This is the customized MS Teams card title", 93 | MsTeamsError: "This is the customized MS Teams card error message", 94 | } 95 | 96 | //Create New Alert 97 | alert := n.NewAlertWithExpandos(err, ignoringErr, expandos) 98 | 99 | //Send notification 100 | alert.Notify() 101 | 102 | // To remove all current throttling 103 | alert.RemoveCurrentThrotting() 104 | 105 | ``` -------------------------------------------------------------------------------- /throttler.go: -------------------------------------------------------------------------------- 1 | package alertnotification 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | "time" 8 | 9 | "github.com/GitbookIO/diskache" 10 | ) 11 | 12 | // Throttler struct storing disckage directory and Throttling duration 13 | type Throttler struct { 14 | CacheOpt string 15 | ThrottleDuration int 16 | GraceDuration int 17 | } 18 | 19 | // ErrorOccurrence store error time and error 20 | type ErrorOccurrence struct { 21 | StartTime time.Time 22 | ErrorType error 23 | } 24 | 25 | // NewThrottler constructs new Throttle struct and init diskcache directory 26 | func NewThrottler() Throttler { 27 | 28 | t := Throttler{ 29 | CacheOpt: fmt.Sprintf("/tmp/cache/%v_throttler_disk_cache", os.Getenv("APP_NAME")), 30 | ThrottleDuration: 5, // default 5mn 31 | GraceDuration: 0, // default 0sc 32 | } 33 | if len(os.Getenv("THROTTLE_DURATION")) != 0 { 34 | duration, err := strconv.Atoi(os.Getenv("THROTTLE_DURATION")) 35 | if err != nil { 36 | return t 37 | } 38 | t.ThrottleDuration = duration 39 | } 40 | if len(os.Getenv("THROTTLE_GRACE_SECONDS")) != 0 { 41 | grace, err := strconv.Atoi(os.Getenv("THROTTLE_GRACE_SECONDS")) 42 | if err != nil { 43 | return t 44 | } 45 | t.GraceDuration = grace 46 | } 47 | 48 | if len(os.Getenv("THROTTLE_DISKCACHE_DIR")) != 0 { 49 | t.CacheOpt = os.Getenv("THROTTLE_DISKCACHE_DIR") 50 | } 51 | 52 | return t 53 | } 54 | 55 | // IsThrottled checks if the error has been throttled. If not, throttle it 56 | func (t *Throttler) IsThrottledOrGraced(ocError error) bool { 57 | dc, err := t.getDiskCache() 58 | if err != nil { 59 | return false 60 | } 61 | cachedThrottleTime, throttled := dc.Get(ocError.Error()) 62 | cachedDetectionTime, graced := dc.Get(fmt.Sprintf("%v_detectionTime", ocError.Error())) 63 | 64 | throttleIsOver := isOverThrottleDuration(string(cachedThrottleTime), t.ThrottleDuration) 65 | if throttled && !throttleIsOver { 66 | // already throttled and not over throttling duration, do nothing 67 | return true 68 | } 69 | 70 | if !graced || isOverGracePlusThrottleDuration(string(cachedDetectionTime), t.GraceDuration, t.ThrottleDuration) { 71 | cachedDetectionTime = t.InitGrace(ocError) 72 | } 73 | if cachedDetectionTime != nil && !isOverGraceDuration(string(cachedDetectionTime), t.GraceDuration) { 74 | // grace duration is not over yet, do nothing 75 | return true 76 | } 77 | 78 | // if it has not throttled yet or over throttle duration, throttle it and return false to send notification 79 | // Rethrottler will also renew the timestamp in the throttler cache. 80 | if err = t.ThrottleError(ocError); err != nil { 81 | return false 82 | } 83 | return false 84 | } 85 | 86 | func isOverGracePlusThrottleDuration(cachedTime string, graceDurationInSec int, throttleDurationInMin int) bool { 87 | detectionTime, err := time.Parse(time.RFC3339, string(cachedTime)) 88 | if err != nil { 89 | return false 90 | } 91 | now := time.Now() 92 | diff := int(now.Sub(detectionTime).Seconds()) 93 | overallDurationInSec := graceDurationInSec + throttleDurationInMin*60 94 | return diff >= overallDurationInSec 95 | } 96 | 97 | func isOverGraceDuration(cachedTime string, graceDuration int) bool { 98 | detectionTime, err := time.Parse(time.RFC3339, string(cachedTime)) 99 | if err != nil { 100 | return false 101 | } 102 | now := time.Now() 103 | diff := int(now.Sub(detectionTime).Seconds()) 104 | return diff >= graceDuration 105 | } 106 | 107 | func isOverThrottleDuration(cachedTime string, throttleDuration int) bool { 108 | throttledTime, err := time.Parse(time.RFC3339, string(cachedTime)) 109 | if err != nil { 110 | return false 111 | } 112 | now := time.Now() 113 | diff := int(now.Sub(throttledTime).Minutes()) 114 | return diff >= throttleDuration 115 | } 116 | 117 | // ThrottleError throttle the alert within the limited duration 118 | func (t *Throttler) ThrottleError(errObj error) error { 119 | dc, err := t.getDiskCache() 120 | if err != nil { 121 | return err 122 | } 123 | 124 | now := time.Now().Format(time.RFC3339) 125 | err = dc.Set(errObj.Error(), []byte(now)) 126 | 127 | return err 128 | } 129 | 130 | // ThrottleError throttle the alert within the limited duration 131 | func (t *Throttler) InitGrace(errObj error) []byte { 132 | dc, err := t.getDiskCache() 133 | if err != nil { 134 | return nil 135 | } 136 | now := time.Now().Format(time.RFC3339) 137 | cachedDetectionTime := []byte(now) 138 | err = dc.Set(fmt.Sprintf("%v_detectionTime", errObj.Error()), cachedDetectionTime) 139 | if err != nil { 140 | return nil 141 | } 142 | 143 | return cachedDetectionTime 144 | } 145 | 146 | // CleanThrottlingCache clean all the diskcache in throttling cache directory 147 | func (t *Throttler) CleanThrottlingCache() (err error) { 148 | dc, err := t.getDiskCache() 149 | if err != nil { 150 | return err 151 | } 152 | err = dc.Clean() 153 | return err 154 | } 155 | 156 | func (t *Throttler) getDiskCache() (*diskache.Diskache, error) { 157 | opts := diskache.Opts{ 158 | Directory: t.CacheOpt, 159 | } 160 | return diskache.New(&opts) 161 | } 162 | -------------------------------------------------------------------------------- /ms_teams.go: -------------------------------------------------------------------------------- 1 | package alertnotification 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "net/url" 11 | "os" 12 | "time" 13 | ) 14 | 15 | // MsTeam is Adaptive Card for Team notification 16 | type MsTeam struct { 17 | Type string `json:"type"` 18 | Attachments []attachment `json:"attachments"` 19 | } 20 | 21 | type attachment struct { 22 | ContentType string `json:"contentType"` 23 | ContentURL *string `json:"contentUrl"` 24 | Content cardContent `json:"content"` 25 | } 26 | 27 | type cardContent struct { 28 | Schema string `json:"$schema"` 29 | Type string `json:"type"` 30 | Version string `json:"version"` 31 | AccentColor string `json:"accentColor"` 32 | Body []interface{} `json:"body"` 33 | Actions []action `json:"actions"` 34 | MSTeams msTeams `json:"msteams"` 35 | } 36 | 37 | type textBlock struct { 38 | Type string `json:"type"` 39 | Text string `json:"text"` 40 | ID string `json:"id,omitempty"` 41 | Size string `json:"size,omitempty"` 42 | Weight string `json:"weight,omitempty"` 43 | Color string `json:"color,omitempty"` 44 | } 45 | 46 | type fact struct { 47 | Title string `json:"title"` 48 | Value string `json:"value"` 49 | } 50 | 51 | type factSet struct { 52 | Type string `json:"type"` 53 | Facts []fact `json:"facts"` 54 | ID string `json:"id"` 55 | } 56 | 57 | type codeBlock struct { 58 | Type string `json:"type"` 59 | CodeSnippet string `json:"codeSnippet"` 60 | FontType string `json:"fontType"` 61 | Wrap bool `json:"wrap"` 62 | } 63 | 64 | type action struct { 65 | Type string `json:"type"` 66 | Title string `json:"title"` 67 | URL string `json:"url"` 68 | } 69 | 70 | type msTeams struct { 71 | Width string `json:"width"` 72 | } 73 | 74 | // NewMsTeam is used to create MsTeam 75 | func NewMsTeam(err error, expandos *Expandos) MsTeam { 76 | title := os.Getenv("ALERT_CARD_SUBJECT") 77 | summary := os.Getenv("MS_TEAMS_CARD_SUBJECT") 78 | errMsg := fmt.Sprintf("%+v", err) 79 | hostname, err := os.Hostname() 80 | if err != nil { 81 | hostname = "hostname_unknown" 82 | } 83 | hostname += " " + os.Getenv("APP_NAME") 84 | // apply expandos on card 85 | if expandos != nil { 86 | if expandos.MsTeamsAlertCardSubject != "" { 87 | title = expandos.MsTeamsAlertCardSubject 88 | } 89 | if expandos.MsTeamsCardSubject != "" { 90 | summary = expandos.MsTeamsCardSubject 91 | } 92 | if expandos.MsTeamsError != "" { 93 | errMsg = expandos.MsTeamsError 94 | } 95 | } 96 | 97 | return MsTeam{ 98 | Type: "message", 99 | Attachments: []attachment{ 100 | { 101 | ContentType: "application/vnd.microsoft.card.adaptive", 102 | ContentURL: nil, 103 | Content: cardContent{ 104 | Schema: "http://adaptivecards.io/schemas/adaptive-card.json", 105 | Type: "AdaptiveCard", 106 | Version: "1.4", 107 | AccentColor: "bf0000", 108 | Body: []interface{}{ 109 | textBlock{ 110 | Type: "TextBlock", 111 | Text: title, 112 | ID: "title", 113 | Size: "large", 114 | Weight: "bolder", 115 | Color: "accent", 116 | }, 117 | factSet{ 118 | Type: "FactSet", 119 | Facts: []fact{ 120 | { 121 | Title: "Title:", 122 | Value: title, 123 | }, 124 | { 125 | Title: "Summary:", 126 | Value: summary, 127 | }, 128 | { 129 | Title: "Hostname:", 130 | Value: hostname, 131 | }, 132 | }, 133 | ID: "acFactSet", 134 | }, 135 | codeBlock{ 136 | Type: "CodeBlock", 137 | CodeSnippet: errMsg, 138 | FontType: "monospace", 139 | Wrap: true, 140 | }, 141 | }, 142 | MSTeams: msTeams{ 143 | Width: "Full", 144 | }, 145 | }, 146 | }, 147 | }, 148 | } 149 | } 150 | 151 | // Send is implementation of interface AlertNotification's Send() 152 | func (card *MsTeam) Send() (err error) { 153 | requestBody, err := json.Marshal(card) 154 | if err != nil { 155 | return err 156 | } 157 | 158 | var client http.Client 159 | timeout := time.Duration(5 * time.Second) 160 | proxyURL := os.Getenv("MS_TEAMS_PROXY_URL") 161 | 162 | if proxyURL != "" { 163 | proxy, err := url.Parse(proxyURL) 164 | if err != nil { 165 | return err 166 | } 167 | transport := &http.Transport{Proxy: http.ProxyURL(proxy)} 168 | client = http.Client{ 169 | Transport: transport, 170 | Timeout: timeout, 171 | } 172 | } else { 173 | client = http.Client{ 174 | Timeout: timeout, 175 | } 176 | } 177 | 178 | wb := os.Getenv("MS_TEAMS_WEBHOOK") 179 | if len(wb) == 0 { 180 | return errors.New("cannot send alert to MSTeams.MS_TEAMS_WEBHOOK is not set in the environment. ") 181 | } 182 | request, err := http.NewRequest("POST", wb, bytes.NewBuffer(requestBody)) 183 | request.Header.Set("Content-type", "application/json") 184 | if err != nil { 185 | return err 186 | } 187 | 188 | resp, err := client.Do(request) 189 | if err != nil { 190 | return err 191 | } 192 | 193 | defer resp.Body.Close() 194 | 195 | if resp.StatusCode != http.StatusAccepted { 196 | respBody, err := io.ReadAll(io.LimitReader(resp.Body, 1024*1024)) 197 | if err != nil { 198 | return err 199 | } 200 | return fmt.Errorf("unexpected response from webhook: %s", string(respBody)) 201 | } 202 | return 203 | } 204 | -------------------------------------------------------------------------------- /alert_test.go: -------------------------------------------------------------------------------- 1 | package alertnotification 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "testing" 8 | 9 | "github.com/joho/godotenv" 10 | ) 11 | 12 | func setEnv() { 13 | if err := godotenv.Load("test.env"); err != nil { 14 | fmt.Println(err) 15 | } 16 | } 17 | 18 | func Test_shouldMsTeams(t *testing.T) { 19 | tests := []struct { 20 | name string 21 | want bool 22 | }{ 23 | {name: "shouldSend", want: true}, 24 | {name: "shouldNotSend", want: false}, 25 | } 26 | 27 | for _, tt := range tests { 28 | setEnv() 29 | switch tt.name { 30 | case "shouldSend": 31 | os.Setenv("EMAIL_ALERT_ENABLED", "true") 32 | os.Setenv("MS_TEAMS_ALERT_ENABLED", "true") 33 | case "shouldNotSend": 34 | os.Setenv("MS_TEAMS_ALERT_ENABLED", "") 35 | } 36 | t.Run(tt.name, func(t *testing.T) { 37 | 38 | if got := shouldMsTeams(); got != tt.want { 39 | t.Errorf("shouldMsTeams() = %v, want %v", got, tt.want) 40 | } 41 | }) 42 | } 43 | } 44 | 45 | func Test_shouldMail(t *testing.T) { 46 | tests := []struct { 47 | name string 48 | want bool 49 | }{ 50 | 51 | {name: "shouldSend", want: true}, 52 | {name: "shouldNotSend", want: false}, 53 | } 54 | 55 | for _, tt := range tests { 56 | setEnv() 57 | switch tt.name { 58 | case "shouldSend": 59 | os.Setenv("EMAIL_ALERT_ENABLED", "true") 60 | case "shouldNotSend": 61 | os.Setenv("EMAIL_ALERT_ENABLED", "") 62 | } 63 | t.Run(tt.name, func(t *testing.T) { 64 | if got := shouldMail(); got != tt.want { 65 | t.Errorf("shouldMail() = %v, want %v", got, tt.want) 66 | } 67 | }) 68 | } 69 | } 70 | 71 | func TestAlert_Notify(t *testing.T) { 72 | expandos := &Expandos{ 73 | EmailBody: "TEST: This is mail body", 74 | EmailSubject: "TEST: This is mail subject", 75 | MsTeamsAlertCardSubject: "TEST: This is MS Teams card title", 76 | MsTeamsCardSubject: "TEST: This is MS Teams card summary", 77 | MsTeamsError: "TEST: This is MS Teams card error message", 78 | } 79 | type fields struct { 80 | Error error 81 | DoNotAlertErrors []error 82 | Expandos *Expandos 83 | } 84 | tests := []struct { 85 | name string 86 | fields fields 87 | wantErr bool 88 | }{ 89 | {name: "Notify_false", 90 | fields: fields{ 91 | Error: errors.New("Do not alert"), // Do not alert => no error will occur 92 | DoNotAlertErrors: []error{ 93 | errors.New("Do not alert"), errors.New("if this error then alert")}, 94 | }, 95 | wantErr: false, 96 | }, 97 | {name: "Notify_true", 98 | fields: fields{ 99 | Error: errors.New("give an alert"), // error occured and try to send email => no mail setting configure => error. 100 | DoNotAlertErrors: []error{ 101 | errors.New("Do not alert"), errors.New("if this error then alert")}, 102 | }, 103 | wantErr: true, 104 | }, 105 | {name: "Expandos", 106 | fields: fields{ 107 | Error: errors.New("give an alert"), // error occured and try to send email => no mail setting configure => error. 108 | DoNotAlertErrors: []error{ 109 | errors.New("Do not alert"), errors.New("if this error then alert")}, 110 | Expandos: expandos, 111 | }, 112 | wantErr: true, 113 | }, 114 | } 115 | for _, tt := range tests { 116 | if err := godotenv.Overload("test.env"); err != nil { // Reload Env 117 | fmt.Println(err) 118 | } 119 | t.Run(tt.name, func(t *testing.T) { 120 | 121 | a := &Alert{ 122 | Error: tt.fields.Error, 123 | DoNotAlertErrors: tt.fields.DoNotAlertErrors, 124 | Expandos: tt.fields.Expandos, 125 | } 126 | if err := a.RemoveCurrentThrotting(); err != nil { 127 | t.Errorf("Alert.Notify() error = %+v", err) 128 | } 129 | if err := a.Notify(); (err != nil) != tt.wantErr { 130 | t.Errorf("Alert.Notify() error = %v, wantErr %v", err, tt.wantErr) 131 | } 132 | }) 133 | } 134 | } 135 | 136 | func TestAlert_shouldAlert(t *testing.T) { 137 | type fields struct { 138 | Error error 139 | DoNotAlertErrors []error 140 | } 141 | tests := []struct { 142 | name string 143 | fields fields 144 | want bool 145 | }{ 146 | {name: "shouldAlert_false", 147 | fields: fields{ 148 | Error: errors.New("Do not alert"), 149 | DoNotAlertErrors: []error{ 150 | errors.New("Do not alert"), errors.New("if this error then don't alert")}, 151 | }, 152 | want: false, 153 | }, 154 | {name: "shouldAlert_true", 155 | fields: fields{ 156 | Error: errors.New("alert this"), 157 | DoNotAlertErrors: []error{ 158 | errors.New("do not alert"), errors.New("if this error then don't alert")}, 159 | }, 160 | want: true, 161 | }, 162 | {name: "shouldAlert_graced_false", 163 | fields: fields{ 164 | Error: errors.New("alert this"), 165 | DoNotAlertErrors: []error{ 166 | errors.New("do not alert"), errors.New("if this error then don't alert")}, 167 | }, 168 | want: false, 169 | }, 170 | {name: "shouldAlert_true_disable_throttling", 171 | fields: fields{ 172 | Error: errors.New("do not alert"), 173 | DoNotAlertErrors: []error{ 174 | errors.New("do not alert"), errors.New("if this error then don't alert")}, 175 | }, 176 | want: true, 177 | }, 178 | } 179 | for _, tt := range tests { 180 | t.Run(tt.name, func(t *testing.T) { 181 | if tt.name == "shouldAlert_true_disable_throttling" { 182 | os.Setenv("THROTTLE_ENABLED", "false") 183 | } 184 | if tt.name == "shouldAlert_graced_false" { 185 | os.Setenv("THROTTLE_GRACE_SECONDS", "20") 186 | } 187 | a := &Alert{ 188 | Error: tt.fields.Error, 189 | DoNotAlertErrors: tt.fields.DoNotAlertErrors, 190 | } 191 | if err := a.RemoveCurrentThrotting(); err != nil { 192 | t.Errorf("Alert.Notify() error = %+v", err) 193 | } 194 | got := a.shouldAlert() 195 | if got != tt.want { 196 | t.Errorf("Alert.shouldAlert() = %v, want %v", got, tt.want) 197 | } 198 | }) 199 | } 200 | } 201 | 202 | func TestAlert_isDoNotAlert(t *testing.T) { 203 | type fields struct { 204 | Error error 205 | DoNotAlertErrors []error 206 | } 207 | tests := []struct { 208 | name string 209 | fields fields 210 | want bool 211 | }{ 212 | {name: "isDoNotAlert_true", 213 | fields: fields{ 214 | Error: errors.New("Do not alert"), 215 | DoNotAlertErrors: []error{ 216 | errors.New("Do not alert"), errors.New("if this error then not alert")}, 217 | }, 218 | want: true, 219 | }, 220 | {name: "isDoNotAlert_false", 221 | fields: fields{ 222 | Error: errors.New("give an alert"), 223 | DoNotAlertErrors: []error{ 224 | errors.New("Do not alert"), errors.New("if this error then do alert")}, 225 | }, 226 | want: false, 227 | }, 228 | } 229 | for _, tt := range tests { 230 | t.Run(tt.name, func(t *testing.T) { 231 | a := &Alert{ 232 | Error: tt.fields.Error, 233 | DoNotAlertErrors: tt.fields.DoNotAlertErrors, 234 | } 235 | if got := a.isDoNotAlert(); got != tt.want { 236 | t.Errorf("Alert.isDoNotAlert() = %v, want %v", got, tt.want) 237 | } 238 | }) 239 | } 240 | } 241 | 242 | func TestAlert_isThrottlingEnabled(t *testing.T) { 243 | type fields struct { 244 | Error error 245 | DoNotAlertErrors []error 246 | } 247 | tests := []struct { 248 | name string 249 | fields fields 250 | want bool 251 | }{ 252 | {name: "isThrottlingEnabled_true", 253 | fields: fields{ 254 | Error: errors.New("Do not alert"), 255 | DoNotAlertErrors: []error{ 256 | errors.New("Do not alert"), errors.New("if this error then not alert")}, 257 | }, 258 | want: true, 259 | }, 260 | {name: "isThrottlingEnabled_false", 261 | fields: fields{ 262 | Error: errors.New("give an alert"), 263 | DoNotAlertErrors: []error{ 264 | errors.New("Do not alert"), errors.New("if this error then do alert")}, 265 | }, 266 | want: false, 267 | }, 268 | } 269 | for _, tt := range tests { 270 | t.Run(tt.name, func(t *testing.T) { 271 | os.Setenv("THROTTLE_ENABLED", "true") 272 | if tt.name == "isThrottlingEnabled_false" { 273 | os.Setenv("THROTTLE_ENABLED", "false") 274 | } 275 | a := &Alert{ 276 | Error: tt.fields.Error, 277 | DoNotAlertErrors: tt.fields.DoNotAlertErrors, 278 | } 279 | if got := a.isThrottlingEnabled(); got != tt.want { 280 | t.Errorf("Alert.isThrottlingEnabled() = %v, want %v", got, tt.want) 281 | } 282 | }) 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /throttler_test.go: -------------------------------------------------------------------------------- 1 | package alertnotification 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "reflect" 8 | "testing" 9 | "time" 10 | 11 | "github.com/GitbookIO/diskache" 12 | ) 13 | 14 | func TestNewThrottler(t *testing.T) { 15 | tests := []struct { 16 | name string 17 | want Throttler 18 | }{ 19 | { 20 | name: "default", 21 | want: Throttler{ 22 | CacheOpt: fmt.Sprintf("/tmp/cache/%v_throttler_disk_cache", os.Getenv("APP_NAME")), 23 | ThrottleDuration: 5, 24 | GraceDuration: 0, 25 | }, 26 | }, 27 | { 28 | name: "change duration", 29 | want: Throttler{ 30 | CacheOpt: fmt.Sprintf("/tmp/cache/%v_throttler_disk_cache", os.Getenv("APP_NAME")), 31 | ThrottleDuration: 7, 32 | GraceDuration: 5, 33 | }, 34 | }, 35 | { 36 | name: "change both", 37 | want: Throttler{ 38 | CacheOpt: "new_cache_dir", 39 | ThrottleDuration: 8, 40 | GraceDuration: 0, 41 | }, 42 | }, 43 | } 44 | for _, tt := range tests { 45 | if tt.name == "change duration" { 46 | os.Setenv("THROTTLE_DURATION", "7") 47 | os.Setenv("THROTTLE_GRACE_SECONDS", "5") 48 | } else if tt.name == "change both" { 49 | os.Setenv("THROTTLE_DURATION", "8") 50 | os.Setenv("THROTTLE_GRACE_SECONDS", "0") 51 | os.Setenv("THROTTLE_DISKCACHE_DIR", "new_cache_dir") 52 | } else if tt.name == "default" { 53 | os.Setenv("THROTTLE_GRACE_SECONDS", "") 54 | } 55 | t.Run(tt.name, func(t *testing.T) { 56 | got := NewThrottler() 57 | if !reflect.DeepEqual(got, tt.want) { 58 | t.Errorf("NewThrottler() = %v, want %v", got, tt.want) 59 | } 60 | }) 61 | } 62 | } 63 | 64 | func TestThrottler_IsThrottledOrGraced(t *testing.T) { 65 | type fields struct { 66 | CacheOpt string 67 | ThrottleDuration int 68 | GraceDuration int 69 | } 70 | type args struct { 71 | ocError error 72 | } 73 | tests := []struct { 74 | name string 75 | fields fields 76 | args args 77 | want bool 78 | }{ 79 | { 80 | name: "default", 81 | fields: fields{ 82 | CacheOpt: fmt.Sprintf("/tmp/cache/%v_throttler_disk_cache", os.Getenv("APP_NAME")), 83 | ThrottleDuration: 5, 84 | GraceDuration: 0, 85 | }, 86 | args: args{ 87 | ocError: errors.New("test_throttling"), 88 | }, 89 | want: false, 90 | }, 91 | { 92 | name: "throttled_true", 93 | fields: fields{ 94 | CacheOpt: fmt.Sprintf("/tmp/cache/%v_throttler_disk_cache", os.Getenv("APP_NAME")), 95 | ThrottleDuration: 5, 96 | GraceDuration: 0, 97 | }, 98 | args: args{ 99 | ocError: errors.New("test_throttling"), 100 | }, 101 | want: true, 102 | }, 103 | { 104 | name: "graced_true", 105 | fields: fields{ 106 | CacheOpt: fmt.Sprintf("/tmp/cache/%v_throttler_disk_cache", os.Getenv("APP_NAME")), 107 | ThrottleDuration: 5, 108 | GraceDuration: 25, 109 | }, 110 | args: args{ 111 | ocError: errors.New("test_throttling"), 112 | }, 113 | want: true, 114 | }, 115 | } 116 | for _, tt := range tests { 117 | t.Run(tt.name, func(t *testing.T) { 118 | th := &Throttler{ 119 | CacheOpt: tt.fields.CacheOpt, 120 | ThrottleDuration: tt.fields.ThrottleDuration, 121 | GraceDuration: tt.fields.GraceDuration, 122 | } 123 | if tt.name == "throttled_true" { 124 | if err := th.ThrottleError(tt.args.ocError); err != nil { 125 | t.Errorf("testing failed : %+v", err) 126 | } 127 | } 128 | if got := th.IsThrottledOrGraced(tt.args.ocError); got != tt.want { 129 | t.Errorf("Throttler.IsThrottled() = %v, want %v", got, tt.want) 130 | } 131 | err := th.CleanThrottlingCache() 132 | if err != nil { 133 | t.Errorf("Cannot clean after test. %+v", err) 134 | } 135 | 136 | }) 137 | } 138 | } 139 | 140 | func TestThrottler_ThrottleError(t *testing.T) { 141 | type fields struct { 142 | CacheOpt string 143 | ThrottleDuration int 144 | GraceDuration int 145 | } 146 | type args struct { 147 | errObj error 148 | } 149 | tests := []struct { 150 | name string 151 | fields fields 152 | args args 153 | wantErr bool 154 | }{ 155 | { 156 | name: "default", 157 | fields: fields{ 158 | CacheOpt: fmt.Sprintf("/tmp/cache/%v_throttler_disk_cache", os.Getenv("APP_NAME")), 159 | ThrottleDuration: 5, 160 | GraceDuration: 0, 161 | }, 162 | args: args{ 163 | errObj: errors.New("test_throttling"), 164 | }, 165 | wantErr: false, 166 | }, 167 | { 168 | name: "test_error", 169 | fields: fields{ 170 | CacheOpt: "/no_permission_dir", 171 | ThrottleDuration: 5, 172 | GraceDuration: 0, 173 | }, 174 | args: args{ 175 | errObj: errors.New("test_throttling"), 176 | }, 177 | wantErr: true, 178 | }, 179 | } 180 | for _, tt := range tests { 181 | 182 | t.Run(tt.name, func(t *testing.T) { 183 | th := &Throttler{ 184 | CacheOpt: tt.fields.CacheOpt, 185 | ThrottleDuration: tt.fields.ThrottleDuration, 186 | } 187 | 188 | if err := th.ThrottleError(tt.args.errObj); (err != nil) != tt.wantErr { 189 | t.Errorf("Throttler.ThrottleError() error = %v, wantErr %v", err, tt.wantErr) 190 | } 191 | if tt.name == "default" && !th.IsThrottledOrGraced(tt.args.errObj) { 192 | t.Errorf("Throttler.ThrottleError() error = %v, wantErr %v", errors.New("throttling failed"), tt.wantErr) 193 | } 194 | if !tt.wantErr { 195 | err := th.CleanThrottlingCache() 196 | if err != nil { 197 | t.Errorf("Cannot clean after test. %+v", err) 198 | } 199 | } 200 | 201 | }) 202 | } 203 | } 204 | 205 | func TestThrottler_getDiskCache(t *testing.T) { 206 | type fields struct { 207 | CacheOpt string 208 | ThrottleDuration int 209 | } 210 | cachePart := fmt.Sprintf("/tmp/cache/%v_throttler_disk_cache", os.Getenv("APP_NAME")) 211 | opts := diskache.Opts{ 212 | Directory: cachePart, 213 | } 214 | dc, err := diskache.New(&opts) 215 | if err != nil { 216 | t.Errorf("Throttler.getDiskCache() error = %v", err) 217 | return 218 | } 219 | 220 | tests := []struct { 221 | name string 222 | fields fields 223 | want *diskache.Diskache 224 | wantErr bool 225 | }{ 226 | { 227 | name: "TestThrottler_getDiskCache_success", 228 | fields: fields{ 229 | CacheOpt: cachePart, 230 | ThrottleDuration: 5, 231 | }, 232 | want: dc, 233 | wantErr: false, 234 | }, 235 | } 236 | for _, tt := range tests { 237 | t.Run(tt.name, func(t *testing.T) { 238 | th := &Throttler{ 239 | CacheOpt: tt.fields.CacheOpt, 240 | ThrottleDuration: tt.fields.ThrottleDuration, 241 | } 242 | got, err := th.getDiskCache() 243 | if (err != nil) != tt.wantErr { 244 | t.Errorf("Throttler.getDiskCache() error = %v, wantErr %v", err, tt.wantErr) 245 | return 246 | } 247 | if !reflect.DeepEqual(got, tt.want) { 248 | t.Errorf("Throttler.getDiskCache() = %v, want %v", got, tt.want) 249 | } 250 | }) 251 | } 252 | } 253 | 254 | func Test_isOverThrottleDuration(t *testing.T) { 255 | type args struct { 256 | cachedTime string 257 | throttleDuration int 258 | graceDuration int 259 | } 260 | tests := []struct { 261 | name string 262 | args args 263 | want bool 264 | }{ 265 | { 266 | name: "Test_isOverThrottleDuration_true", 267 | args: args{ 268 | cachedTime: time.Now().Add(-3 * time.Minute).Format(time.RFC3339), // -3 minutes => pass 2 minutes durations 269 | throttleDuration: 2, 270 | graceDuration: 0, 271 | }, 272 | want: true, 273 | }, 274 | { 275 | name: "Test_isOverThrottleDuration_false", 276 | args: args{ 277 | cachedTime: time.Now().Add(1 * time.Minute).Format(time.RFC3339), // 1 minute ahead of current < throtte duration 2 278 | throttleDuration: 2, 279 | graceDuration: 0, 280 | }, 281 | want: false, 282 | }, 283 | } 284 | for _, tt := range tests { 285 | t.Run(tt.name, func(t *testing.T) { 286 | if got := isOverThrottleDuration(tt.args.cachedTime, tt.args.throttleDuration); got != tt.want { 287 | t.Errorf("isOverThrottleDuration() = %v, want %v", got, tt.want) 288 | } 289 | }) 290 | } 291 | } 292 | 293 | func Test_isOverGraceDuration(t *testing.T) { 294 | type args struct { 295 | cachedTime string 296 | throttleDuration int 297 | graceDuration int 298 | } 299 | tests := []struct { 300 | name string 301 | args args 302 | want bool 303 | }{ 304 | { 305 | name: "Test_isOverGraceDuration_true", 306 | args: args{ 307 | cachedTime: time.Now().Add(-5 * time.Second).Format(time.RFC3339), // 2 sec after grace duration is over 308 | throttleDuration: 0, 309 | graceDuration: 3, 310 | }, 311 | want: true, 312 | }, 313 | { 314 | name: "Test_isOverGraceDuration_false", 315 | args: args{ 316 | cachedTime: time.Now().Add(2 * time.Second).Format(time.RFC3339), // still 8 sec left for grace duration 317 | throttleDuration: 0, 318 | graceDuration: 10, 319 | }, 320 | want: false, 321 | }, 322 | } 323 | for _, tt := range tests { 324 | t.Run(tt.name, func(t *testing.T) { 325 | if got := isOverGraceDuration(tt.args.cachedTime, tt.args.graceDuration); got != tt.want { 326 | t.Errorf("isOverGraceDuration() = %v, want %v", got, tt.want) 327 | } 328 | }) 329 | } 330 | 331 | } 332 | --------------------------------------------------------------------------------