├── renovate.json ├── assets └── img │ ├── logo.png │ └── logo2.png ├── .gitignore ├── hippo.go ├── go.mod ├── hippo_test.go ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── CONTRIBUTING.md ├── correlation.go ├── config.toml ├── .mergify.yml ├── .travis.yml ├── LICENSE ├── workers_pool.go ├── api_rate_limiter.go ├── logger.go ├── health_test.go ├── process_rate_limiter.go ├── caching_test.go ├── latency.go ├── Makefile ├── system_stats.go ├── CODE_OF_CONDUCT.md ├── health.go ├── caching.go ├── time_series.go ├── service_discovery.go ├── service_discovery_test.go ├── http.go ├── http_test.go ├── README.md └── go.sum /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /assets/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clivern/Hippo/HEAD/assets/img/logo.png -------------------------------------------------------------------------------- /assets/img/logo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clivern/Hippo/HEAD/assets/img/logo2.png -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /hippo.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Clivern. All rights reserved. 2 | // Use of this source code is governed by the MIT 3 | // license that can be found in the LICENSE file. 4 | 5 | package hippo 6 | 7 | // PkgName returns the package name 8 | func PkgName() string { 9 | return "Hippo" 10 | } 11 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/clivern/hippo 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/go-redis/redis v6.15.9+incompatible 7 | github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 8 | github.com/onsi/ginkgo v1.14.0 // indirect 9 | github.com/satori/go.uuid v1.2.0 10 | go.uber.org/zap v1.17.0 11 | golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba 12 | ) 13 | -------------------------------------------------------------------------------- /hippo_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Clivern. All rights reserved. 2 | // Use of this source code is governed by the MIT 3 | // license that can be found in the LICENSE file. 4 | 5 | package hippo 6 | 7 | import ( 8 | "github.com/nbio/st" 9 | "testing" 10 | ) 11 | 12 | // TestPkgName test cases 13 | func TestPkgName(t *testing.T) { 14 | st.Expect(t, PkgName(), "Hippo") 15 | } 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **Development or production environment** 11 | - OS: [e.g. Ubuntu 18.04] 12 | - Go 1.11 13 | 14 | **Additional context** 15 | Add any other context about the problem here. 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Additional context** 14 | Add any other context or screenshots about the feature request here. 15 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | - With issues: 4 | - Use the search tool before opening a new issue. 5 | - Please provide source code and commit sha if you found a bug. 6 | - Review existing issues and provide feedback or react to them. 7 | 8 | - With pull requests: 9 | - Open your pull request against `master` 10 | - Your pull request should have no more than two commits, if not you should squash them. 11 | - It should pass all tests in the available continuous integrations systems such as TravisCI. 12 | - You should add/modify tests to cover your proposed code changes. 13 | - If your pull request contains a new feature, please document it on the README. 14 | -------------------------------------------------------------------------------- /correlation.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Clivern. All rights reserved. 2 | // Use of this source code is governed by the MIT 3 | // license that can be found in the LICENSE file. 4 | 5 | package hippo 6 | 7 | import ( 8 | "github.com/satori/go.uuid" 9 | ) 10 | 11 | // Correlation interface 12 | type Correlation interface { 13 | UUIDv4() string 14 | } 15 | 16 | // Correlation struct 17 | type correlation struct { 18 | } 19 | 20 | // NewCorrelation creates an instance of correlation struct 21 | func NewCorrelation() Correlation { 22 | c := &correlation{} 23 | return c 24 | } 25 | 26 | // UUIDv4 create a UUID version 4 27 | func (c *correlation) UUIDv4() string { 28 | u := uuid.Must(uuid.NewV4(), nil) 29 | return u.String() 30 | } 31 | -------------------------------------------------------------------------------- /config.toml: -------------------------------------------------------------------------------- 1 | ignoreGeneratedHeader = false 2 | severity = "warning" 3 | confidence = 0.8 4 | errorCode = 0 5 | warningCode = 0 6 | 7 | [rule.blank-imports] 8 | [rule.context-as-argument] 9 | [rule.context-keys-type] 10 | [rule.dot-imports] 11 | [rule.error-return] 12 | [rule.error-strings] 13 | [rule.error-naming] 14 | [rule.exported] 15 | [rule.if-return] 16 | [rule.increment-decrement] 17 | [rule.var-naming] 18 | [rule.var-declaration] 19 | [rule.package-comments] 20 | [rule.range] 21 | [rule.receiver-naming] 22 | [rule.time-naming] 23 | [rule.unexported-return] 24 | [rule.indent-error-flow] 25 | [rule.errorf] 26 | [rule.empty-block] 27 | [rule.superfluous-else] 28 | [rule.unused-parameter] 29 | [rule.unreachable-code] 30 | [rule.redefines-builtin-id] -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | --- 2 | pull_request_rules: 3 | - 4 | actions: 5 | merge: 6 | method: squash 7 | conditions: 8 | - author!=Clivern 9 | - approved-reviews-by=Clivern 10 | - label=merge 11 | - status-success=Travis CI - Pull Request 12 | - status-success=Travis CI - Branch 13 | name: "Automatic Merge 🚀" 14 | - 15 | actions: 16 | merge: 17 | method: merge 18 | conditions: 19 | - author=Clivern 20 | - label=merge 21 | name: "Automatic Merge 🚀" 22 | - 23 | actions: 24 | merge: 25 | method: squash 26 | conditions: 27 | - "author=renovate[bot]" 28 | - label=merge 29 | - status-success=Travis CI - Pull Request 30 | - status-success=Travis CI - Branch 31 | name: "Automatic Merge for Renovate PRs 🚀" 32 | - 33 | actions: 34 | comment: 35 | message: "Nice! PR merged successfully." 36 | conditions: 37 | - merged 38 | name: "Merge Done 🚀" 39 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | # needed for the nfpm pipe 4 | addons: 5 | apt: 6 | packages: 7 | - rpm 8 | 9 | services: 10 | - redis-server 11 | 12 | go: 13 | - 1.9.x 14 | - 1.10.x 15 | - 1.11.x 16 | - 1.12.x 17 | - master 18 | 19 | env: 20 | - GO111MODULE=on 21 | 22 | install: true 23 | 24 | # Fix this by renaming the directory before testing. 25 | # https://github.com/travis-ci/travis-ci/issues/4573 26 | script: 27 | - export GOBIN="$GOPATH/bin" 28 | - export PATH="$PATH:$GOBIN" 29 | # Fix dir names 30 | - cd $GOPATH/src/github.com/ 31 | - mv Clivern/Hippo Clivern/hippo 32 | - mv Clivern clivern 33 | - cd clivern/hippo 34 | # Config & execute ci tasks 35 | - make install_revive 36 | - make ci 37 | # Workaround to clear any package used for testing only 38 | - git status 39 | - git diff > diff.log 40 | - cat diff.log 41 | - git clean -fd 42 | - git reset --hard 43 | 44 | matrix: 45 | include: 46 | allow_failures: 47 | - go: 1.9.x 48 | - go: 1.10.x 49 | - go: master 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 A. F 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. 22 | -------------------------------------------------------------------------------- /workers_pool.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Clivern. All rights reserved. 2 | // Use of this source code is governed by the MIT 3 | // license that can be found in the LICENSE file. 4 | 5 | package hippo 6 | 7 | import ( 8 | "sync" 9 | ) 10 | 11 | // WorkersPool struct 12 | type WorkersPool struct { 13 | Tasks []*Task 14 | concurrency int 15 | tasksChan chan *Task 16 | wg sync.WaitGroup 17 | } 18 | 19 | // Task struct 20 | type Task struct { 21 | Err error 22 | Result string 23 | f func() (string, error) 24 | } 25 | 26 | // NewWorkersPool initializes a new pool with the given tasks 27 | func NewWorkersPool(tasks []*Task, concurrency int) *WorkersPool { 28 | return &WorkersPool{ 29 | Tasks: tasks, 30 | concurrency: concurrency, 31 | tasksChan: make(chan *Task), 32 | } 33 | } 34 | 35 | // Run runs all work within the pool and blocks until it's finished. 36 | func (w *WorkersPool) Run() { 37 | for i := 0; i < w.concurrency; i++ { 38 | go w.work() 39 | } 40 | 41 | w.wg.Add(len(w.Tasks)) 42 | for _, task := range w.Tasks { 43 | w.tasksChan <- task 44 | } 45 | 46 | // all workers return 47 | close(w.tasksChan) 48 | 49 | w.wg.Wait() 50 | } 51 | 52 | // The work loop for any single goroutine. 53 | func (w *WorkersPool) work() { 54 | for task := range w.tasksChan { 55 | task.Run(&w.wg) 56 | } 57 | } 58 | 59 | // NewTask initializes a new task based on a given work 60 | func NewTask(f func() (string, error)) *Task { 61 | return &Task{f: f} 62 | } 63 | 64 | // Run runs a Task 65 | func (t *Task) Run(wg *sync.WaitGroup) { 66 | t.Result, t.Err = t.f() 67 | wg.Done() 68 | } 69 | -------------------------------------------------------------------------------- /api_rate_limiter.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Clivern. All rights reserved. 2 | // Use of this source code is governed by the MIT 3 | // license that can be found in the LICENSE file. 4 | 5 | package hippo 6 | 7 | import ( 8 | "golang.org/x/time/rate" 9 | "sync" 10 | "time" 11 | ) 12 | 13 | // caller struct 14 | type caller struct { 15 | limiter *rate.Limiter 16 | lastSeen time.Time 17 | } 18 | 19 | // callers list 20 | var callers = make(map[string]*caller) 21 | 22 | // mtx mutex 23 | var mtx sync.Mutex 24 | 25 | // NewCallerLimiter create a new rate limiter with an identifier 26 | func NewCallerLimiter(identifier string, eventsRate rate.Limit, tokenBurst int) *rate.Limiter { 27 | mtx.Lock() 28 | v, exists := callers[identifier] 29 | if !exists { 30 | mtx.Unlock() 31 | return addCaller(identifier, eventsRate, tokenBurst) 32 | } 33 | 34 | // Update the last seen time for the caller. 35 | v.lastSeen = time.Now() 36 | mtx.Unlock() 37 | return v.limiter 38 | } 39 | 40 | // addCaller add a caller 41 | func addCaller(identifier string, eventsRate rate.Limit, tokenBurst int) *rate.Limiter { 42 | limiter := rate.NewLimiter(eventsRate, tokenBurst) 43 | mtx.Lock() 44 | // Include the current time when creating a new caller. 45 | callers[identifier] = &caller{limiter, time.Now()} 46 | mtx.Unlock() 47 | 48 | return limiter 49 | } 50 | 51 | // CleanupCallers cleans old clients 52 | func CleanupCallers(cleanAfter time.Duration) { 53 | mtx.Lock() 54 | for identifier, v := range callers { 55 | if time.Now().Sub(v.lastSeen) > cleanAfter*time.Second { 56 | delete(callers, identifier) 57 | } 58 | } 59 | mtx.Unlock() 60 | } 61 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Clivern. All rights reserved. 2 | // Use of this source code is governed by the MIT 3 | // license that can be found in the LICENSE file. 4 | 5 | package hippo 6 | 7 | import ( 8 | "encoding/json" 9 | "fmt" 10 | "go.uber.org/zap" 11 | "os" 12 | ) 13 | 14 | // NewLogger returns a logger instance 15 | func NewLogger(level, encoding string, outputPaths []string) (*zap.Logger, error) { 16 | cfg := zap.NewProductionConfig() 17 | 18 | rawJSON := []byte(fmt.Sprintf(`{ 19 | "level": "%s", 20 | "encoding": "%s", 21 | "outputPaths": [] 22 | }`, level, encoding)) 23 | 24 | err := json.Unmarshal(rawJSON, &cfg) 25 | 26 | if err != nil { 27 | panic(err) 28 | } 29 | 30 | cfg.Encoding = encoding 31 | cfg.OutputPaths = outputPaths 32 | 33 | logger, err := cfg.Build() 34 | 35 | if err != nil { 36 | panic(err) 37 | } 38 | 39 | return logger, nil 40 | } 41 | 42 | // PathExists reports whether the path exists 43 | func PathExists(path string) bool { 44 | if _, err := os.Stat(path); os.IsNotExist(err) { 45 | return false 46 | } 47 | return true 48 | } 49 | 50 | // FileExists reports whether the named file exists 51 | func FileExists(path string) bool { 52 | if fi, err := os.Stat(path); err == nil { 53 | if fi.Mode().IsRegular() { 54 | return true 55 | } 56 | } 57 | return false 58 | } 59 | 60 | // DirExists reports whether the dir exists 61 | func DirExists(path string) bool { 62 | if fi, err := os.Stat(path); err == nil { 63 | if fi.Mode().IsDir() { 64 | return true 65 | } 66 | } 67 | return false 68 | } 69 | 70 | // EnsureDir ensures that directory exists 71 | func EnsureDir(dirName string, mode int) (bool, error) { 72 | err := os.MkdirAll(dirName, os.FileMode(mode)) 73 | 74 | if err == nil || os.IsExist(err) { 75 | return true, nil 76 | } 77 | return false, err 78 | } 79 | -------------------------------------------------------------------------------- /health_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Clivern. All rights reserved. 2 | // Use of this source code is governed by the MIT 3 | // license that can be found in the LICENSE file. 4 | 5 | package hippo 6 | 7 | import ( 8 | "context" 9 | "fmt" 10 | "github.com/nbio/st" 11 | "testing" 12 | ) 13 | 14 | // TestHealthCheck test cases 15 | func TestHealthCheck(t *testing.T) { 16 | 17 | healthChecker := NewHealthChecker() 18 | healthChecker.AddCheck("ping_check", func() (bool, error) { 19 | return true, nil 20 | }) 21 | healthChecker.AddCheck("db_check", func() (bool, error) { 22 | return false, fmt.Errorf("Database Down") 23 | }) 24 | healthChecker.RunChecks() 25 | 26 | st.Expect(t, healthChecker.ChecksStatus(), "DOWN") 27 | 28 | report, err := healthChecker.ChecksReport() 29 | 30 | st.Expect(t, report, `[{"id":"ping_check","status":"UP","error":"","result":true},{"id":"db_check","status":"DOWN","error":"Database Down","result":false}]`) 31 | st.Expect(t, err, nil) 32 | 33 | st.Expect(t, healthChecker.IsDown(), true) 34 | st.Expect(t, healthChecker.IsUnknown(), false) 35 | st.Expect(t, healthChecker.IsUp(), false) 36 | 37 | healthChecker.Down() 38 | st.Expect(t, healthChecker.IsDown(), true) 39 | 40 | healthChecker.Unknown() 41 | st.Expect(t, healthChecker.IsUnknown(), true) 42 | 43 | healthChecker.Up() 44 | st.Expect(t, healthChecker.IsUp(), true) 45 | 46 | } 47 | 48 | // TestHTTPCheck test cases 49 | func TestHTTPCheck(t *testing.T) { 50 | healthy, error := HTTPCheck(context.Background(), "httpbin", "https://httpbin.org/status/503", map[string]string{}, map[string]string{}) 51 | st.Expect(t, healthy, false) 52 | st.Expect(t, error.Error(), "Service httpbin is unavailable") 53 | 54 | healthy, error = HTTPCheck(context.Background(), "httpbin", "https://httpbin.org/status/200", map[string]string{}, map[string]string{}) 55 | st.Expect(t, healthy, true) 56 | st.Expect(t, error, nil) 57 | } 58 | 59 | // TestRedisCheck test cases 60 | func TestRedisCheck(t *testing.T) { 61 | healthy, error := RedisCheck("redis", "localhost:6379", "", 0) 62 | st.Expect(t, healthy, true) 63 | st.Expect(t, error, nil) 64 | } 65 | -------------------------------------------------------------------------------- /process_rate_limiter.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Clivern. All rights reserved. 2 | // Use of this source code is governed by the MIT 3 | // license that can be found in the LICENSE file. 4 | 5 | package hippo 6 | 7 | import ( 8 | "sync" 9 | "time" 10 | ) 11 | 12 | // ProcessLimiter interface 13 | type ProcessLimiter interface { 14 | // Take should block to make sure that the RPS is met. 15 | Take() time.Time 16 | } 17 | 18 | // Clock Type 19 | type Clock struct { 20 | } 21 | 22 | // Now get current time 23 | func (c *Clock) Now() time.Time { 24 | return time.Now() 25 | } 26 | 27 | // Sleep sleeps for a time 28 | func (c *Clock) Sleep(d time.Duration) { 29 | time.Sleep(d) 30 | } 31 | 32 | // processLimiter type 33 | type processLimiter struct { 34 | sync.Mutex 35 | last time.Time 36 | sleepFor time.Duration 37 | perRequest time.Duration 38 | maxSlack time.Duration 39 | clock Clock 40 | } 41 | 42 | // NewProcessLimiter create a new process rate limiter 43 | func NewProcessLimiter(rate int) ProcessLimiter { 44 | l := &processLimiter{ 45 | perRequest: time.Second / time.Duration(rate), 46 | maxSlack: -10 * time.Second / time.Duration(rate), 47 | } 48 | l.clock = Clock{} 49 | 50 | return l 51 | } 52 | 53 | // Take sleep for time to limit requests 54 | func (t *processLimiter) Take() time.Time { 55 | t.Lock() 56 | defer t.Unlock() 57 | 58 | now := t.clock.Now() 59 | 60 | // If this is our first request, then we allow it. 61 | if t.last.IsZero() { 62 | t.last = now 63 | return t.last 64 | } 65 | 66 | // sleepFor calculates how much time we should sleep based on 67 | // the perRequest budget and how long the last request took. 68 | // Since the request may take longer than the budget, this number 69 | // can get negative, and is summed across requests. 70 | t.sleepFor += t.perRequest - now.Sub(t.last) 71 | 72 | // We shouldn't allow sleepFor to get too negative, since it would mean that 73 | // a service that slowed down a lot for a short period of time would get 74 | // a much higher RPS following that. 75 | if t.sleepFor < t.maxSlack { 76 | t.sleepFor = t.maxSlack 77 | } 78 | 79 | // If sleepFor is positive, then we should sleep now. 80 | if t.sleepFor > 0 { 81 | t.clock.Sleep(t.sleepFor) 82 | t.last = now.Add(t.sleepFor) 83 | t.sleepFor = 0 84 | } else { 85 | t.last = now 86 | } 87 | 88 | return t.last 89 | } 90 | -------------------------------------------------------------------------------- /caching_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Clivern. All rights reserved. 2 | // Use of this source code is governed by the MIT 3 | // license that can be found in the LICENSE file. 4 | 5 | package hippo 6 | 7 | import ( 8 | "fmt" 9 | "github.com/nbio/st" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | // TestRedis test cases 15 | func TestRedis(t *testing.T) { 16 | 17 | driver := NewRedisDriver("localhost:6379", "", 0) 18 | 19 | ok, err := driver.Connect() 20 | st.Expect(t, ok, true) 21 | st.Expect(t, err, nil) 22 | 23 | ok, err = driver.Ping() 24 | st.Expect(t, ok, true) 25 | st.Expect(t, err, nil) 26 | 27 | // Do Clean 28 | driver.Del("app_name") 29 | driver.HTruncate("configs") 30 | 31 | count, err := driver.Del("app_name") 32 | st.Expect(t, int(count), 0) 33 | st.Expect(t, err, nil) 34 | 35 | ok, err = driver.Set("app_name", "Hippo", 0) 36 | st.Expect(t, ok, true) 37 | st.Expect(t, err, nil) 38 | 39 | ok, err = driver.Exists("app_name") 40 | st.Expect(t, ok, true) 41 | st.Expect(t, err, nil) 42 | 43 | value, err := driver.Get("app_name") 44 | st.Expect(t, value, "Hippo") 45 | st.Expect(t, err, nil) 46 | 47 | count, err = driver.HDel("configs", "app_name") 48 | st.Expect(t, int(count), 0) 49 | st.Expect(t, err, nil) 50 | 51 | ok, err = driver.HSet("configs", "app_name", "Hippo") 52 | st.Expect(t, ok, true) 53 | st.Expect(t, err, nil) 54 | 55 | ok, err = driver.HExists("configs", "app_name") 56 | st.Expect(t, ok, true) 57 | st.Expect(t, err, nil) 58 | 59 | value, err = driver.HGet("configs", "app_name") 60 | st.Expect(t, value, "Hippo") 61 | st.Expect(t, err, nil) 62 | 63 | count, err = driver.HLen("configs") 64 | st.Expect(t, int(count), 1) 65 | st.Expect(t, err, nil) 66 | 67 | count, err = driver.HDel("configs", "app_name") 68 | st.Expect(t, int(count), 1) 69 | st.Expect(t, err, nil) 70 | 71 | count, err = driver.HTruncate("configs") 72 | st.Expect(t, int(count), 0) 73 | st.Expect(t, err, nil) 74 | 75 | c := make(chan string) 76 | 77 | go func() { 78 | c <- "Hello World" 79 | driver.Subscribe("hippo", func(message Message) error { 80 | t.Log(message.Channel) 81 | t.Log(message.Payload) 82 | st.Expect(t, "hippo", message.Channel) 83 | st.Expect(t, "Hello World", message.Payload) 84 | return fmt.Errorf("Terminate listener") 85 | }) 86 | }() 87 | 88 | msg := <-c 89 | time.Sleep(4 * time.Second) 90 | driver.Publish("hippo", msg) 91 | } 92 | -------------------------------------------------------------------------------- /latency.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Clivern. All rights reserved. 2 | // Use of this source code is governed by the MIT 3 | // license that can be found in the LICENSE file. 4 | 5 | package hippo 6 | 7 | import ( 8 | "fmt" 9 | "time" 10 | ) 11 | 12 | // Point struct 13 | type Point struct { 14 | Start time.Time 15 | End time.Time 16 | } 17 | 18 | // Latency struct 19 | type Latency struct { 20 | Actions map[string][]Point 21 | } 22 | 23 | // NewLatencyTracker creates a new latency instance 24 | func NewLatencyTracker() *Latency { 25 | return &Latency{} 26 | } 27 | 28 | // NewAction creates a new action tracking bucket 29 | func (l *Latency) NewAction(name string) { 30 | if len(l.Actions) <= 0 { 31 | l.Actions = make(map[string][]Point) 32 | } 33 | 34 | l.Actions[name] = []Point{} 35 | } 36 | 37 | // SetPoint adds a new point 38 | func (l *Latency) SetPoint(name string, start, end time.Time) { 39 | if _, ok := l.Actions[name]; !ok { 40 | l.NewAction(name) 41 | } 42 | l.Actions[name] = append(l.Actions[name], Point{Start: start, End: end}) 43 | } 44 | 45 | // SetStart adds point start time 46 | func (l *Latency) SetStart(name string, start time.Time) bool { 47 | if _, ok := l.Actions[name]; !ok { 48 | l.NewAction(name) 49 | } 50 | l.Actions[name] = append(l.Actions[name], Point{Start: start}) 51 | 52 | return true 53 | } 54 | 55 | // SetEnd adds point end time 56 | func (l *Latency) SetEnd(name string, end time.Time) bool { 57 | if _, ok := l.Actions[name]; !ok { 58 | l.NewAction(name) 59 | } 60 | 61 | length := len(l.Actions[name]) 62 | 63 | if length <= 0 { 64 | return false 65 | } 66 | 67 | if l.Actions[name][length-1].End.String() == "" { 68 | return false 69 | } 70 | 71 | l.Actions[name][length-1].End = end 72 | 73 | return true 74 | } 75 | 76 | // GetLatency returns average latency in nanoseconds for specific action 77 | func (l *Latency) GetLatency(name string) (time.Duration, error) { 78 | var total time.Duration 79 | 80 | for _, v := range l.Actions[name] { 81 | total += v.GetLatency() 82 | } 83 | 84 | result := total.Nanoseconds() / int64(len(l.Actions[name])) 85 | timeDuration, err := time.ParseDuration(fmt.Sprintf("%dns", result)) 86 | 87 | if err != nil { 88 | return time.Duration(0), err 89 | } 90 | 91 | return timeDuration, nil 92 | } 93 | 94 | // GetLatency returns latency in nanoseconds 95 | func (p *Point) GetLatency() time.Duration { 96 | return p.End.Sub(p.Start) 97 | } 98 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GO ?= go 2 | GOFMT ?= $(GO)fmt 3 | pkgs = ./... 4 | 5 | 6 | help: Makefile 7 | @echo 8 | @echo " Choose a command run in Hippo:" 9 | @echo 10 | @sed -n 's/^##//p' $< | column -t -s ':' | sed -e 's/^/ /' 11 | @echo 12 | 13 | 14 | ## install_revive: Install revive for linting. 15 | install_revive: 16 | @echo ">> ============= Install Revive ============= <<" 17 | $(GO) get github.com/mgechev/revive 18 | 19 | 20 | ## style: Check code style. 21 | style: 22 | @echo ">> ============= Checking Code Style ============= <<" 23 | @fmtRes=$$($(GOFMT) -d $$(find . -path ./vendor -prune -o -name '*.go' -print)); \ 24 | if [ -n "$${fmtRes}" ]; then \ 25 | echo "gofmt checking failed!"; echo "$${fmtRes}"; echo; \ 26 | echo "Please ensure you are using $$($(GO) version) for formatting code."; \ 27 | exit 1; \ 28 | fi 29 | 30 | 31 | ## check_license: Check if license header on all files. 32 | check_license: 33 | @echo ">> ============= Checking License Header ============= <<" 34 | @licRes=$$(for file in $$(find . -type f -iname '*.go' ! -path './vendor/*') ; do \ 35 | awk 'NR<=3' $$file | grep -Eq "(Copyright|generated|GENERATED)" || echo $$file; \ 36 | done); \ 37 | if [ -n "$${licRes}" ]; then \ 38 | echo "license header checking failed:"; echo "$${licRes}"; \ 39 | exit 1; \ 40 | fi 41 | 42 | 43 | ## test_short: Run test cases with short flag. 44 | test_short: 45 | @echo ">> ============= Running Short Tests ============= <<" 46 | $(GO) test -short $(pkgs) 47 | 48 | 49 | ## test: Run test cases. 50 | test: 51 | @echo ">> ============= Running All Tests ============= <<" 52 | $(GO) test -race -cover $(pkgs) 53 | 54 | 55 | ## lint: Lint the code. 56 | lint: 57 | @echo ">> ============= Lint All Files ============= <<" 58 | revive -config config.toml -exclude vendor/... -formatter friendly ./... 59 | 60 | 61 | ## format: Format the code. 62 | format: 63 | @echo ">> ============= Formatting Code ============= <<" 64 | $(GO) fmt $(pkgs) 65 | 66 | 67 | ## vet: Examines source code and reports suspicious constructs. 68 | vet: 69 | @echo ">> ============= Vetting Code ============= <<" 70 | $(GO) vet $(pkgs) 71 | 72 | 73 | ## coverage: Create HTML coverage report 74 | coverage: 75 | @echo ">> ============= Coverage ============= <<" 76 | rm -f coverage.html cover.out 77 | $(GO) test -coverprofile=cover.out $(pkgs) 78 | go tool cover -html=cover.out -o coverage.html 79 | 80 | 81 | ## ci: Run all CI tests. 82 | ci: style check_license test vet lint 83 | @echo "\n==> All quality checks passed" 84 | 85 | 86 | .PHONY: help 87 | -------------------------------------------------------------------------------- /system_stats.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Clivern. All rights reserved. 2 | // Use of this source code is governed by the MIT 3 | // license that can be found in the LICENSE file. 4 | 5 | package hippo 6 | 7 | import ( 8 | "runtime" 9 | "time" 10 | ) 11 | 12 | // SystemStats stuct 13 | type SystemStats struct { 14 | EnableCPU bool 15 | EnableMem bool 16 | EnableGC bool 17 | StartTime time.Time 18 | Stats map[string]uint64 19 | } 20 | 21 | // NewSystemStats creates a new SystemStats 22 | func NewSystemStats(enableCPU, enableMem, enableGC bool) *SystemStats { 23 | return &SystemStats{ 24 | EnableCPU: enableCPU, 25 | EnableMem: enableMem, 26 | EnableGC: enableGC, 27 | } 28 | } 29 | 30 | // Collect collects enabled stats 31 | func (s *SystemStats) Collect() { 32 | s.Stats = make(map[string]uint64) 33 | mStats := runtime.MemStats{} 34 | if s.EnableMem { 35 | s.outputMemStats(&mStats) 36 | } 37 | if s.EnableGC { 38 | s.outputGCStats(&mStats) 39 | } 40 | if s.EnableCPU { 41 | s.outputCPUStats() 42 | } 43 | s.outputTimeStats() 44 | } 45 | 46 | // outputCPUStats sets CPU stats 47 | func (s *SystemStats) outputCPUStats() { 48 | s.append("cpu.goroutines", uint64(runtime.NumGoroutine())) 49 | s.append("cpu.cgo_calls", uint64(runtime.NumCgoCall())) 50 | } 51 | 52 | // outputMemStats sets memory stats 53 | func (s *SystemStats) outputMemStats(m *runtime.MemStats) { 54 | // General 55 | s.append("mem.alloc", m.Alloc) 56 | s.append("mem.total", m.TotalAlloc) 57 | s.append("mem.sys", m.Sys) 58 | s.append("mem.lookups", m.Lookups) 59 | s.append("mem.malloc", m.Mallocs) 60 | s.append("mem.frees", m.Frees) 61 | 62 | // Heap 63 | s.append("mem.heap.alloc", m.HeapAlloc) 64 | s.append("mem.heap.sys", m.HeapSys) 65 | s.append("mem.heap.idle", m.HeapIdle) 66 | s.append("mem.heap.inuse", m.HeapInuse) 67 | s.append("mem.heap.released", m.HeapReleased) 68 | s.append("mem.heap.objects", m.HeapObjects) 69 | 70 | // Stack 71 | s.append("mem.stack.inuse", m.StackInuse) 72 | s.append("mem.stack.sys", m.StackSys) 73 | s.append("mem.stack.mspan_inuse", m.MSpanInuse) 74 | s.append("mem.stack.mspan_sys", m.MSpanSys) 75 | s.append("mem.stack.mcache_inuse", m.MCacheInuse) 76 | s.append("mem.stack.mcache_sys", m.MCacheSys) 77 | s.append("mem.othersys", m.OtherSys) 78 | } 79 | 80 | // outputGCStats sets GC stats 81 | func (s *SystemStats) outputGCStats(m *runtime.MemStats) { 82 | s.append("mem.gc.sys", m.GCSys) 83 | s.append("mem.gc.next", m.NextGC) 84 | s.append("mem.gc.last", m.LastGC) 85 | s.append("mem.gc.pause_total", m.PauseTotalNs) 86 | s.append("mem.gc.pause", m.PauseNs[(m.NumGC+255)%256]) 87 | s.append("mem.gc.count", uint64(m.NumGC)) 88 | } 89 | 90 | // outputTimeStats sets uptime 91 | func (s *SystemStats) outputTimeStats() { 92 | s.append("uptime", uint64(time.Since(s.StartTime).Seconds())) 93 | } 94 | 95 | // GetStats get stats list 96 | func (s *SystemStats) GetStats() map[string]uint64 { 97 | return s.Stats 98 | } 99 | 100 | // append add stats 101 | func (s *SystemStats) append(key string, value uint64) { 102 | s.Stats[key] = value 103 | } 104 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at hello@clivern.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /health.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Clivern. All rights reserved. 2 | // Use of this source code is governed by the MIT 3 | // license that can be found in the LICENSE file. 4 | 5 | package hippo 6 | 7 | import ( 8 | "context" 9 | "encoding/json" 10 | "fmt" 11 | "net/http" 12 | ) 13 | 14 | const ( 15 | // ServiceUp const 16 | ServiceUp = "UP" 17 | // ServiceDown const 18 | ServiceDown = "DOWN" 19 | // ServiceUnknown const 20 | ServiceUnknown = "UNKNOWN" 21 | ) 22 | 23 | // Check struct 24 | type Check struct { 25 | ID string `json:"id"` 26 | Status string `json:"status"` 27 | Error string `json:"error"` 28 | Result bool `json:"result"` 29 | callable func() (bool, error) 30 | } 31 | 32 | // Health struct 33 | type Health struct { 34 | Status string 35 | Checks []*Check 36 | } 37 | 38 | // NewHealthChecker initializes a new health checker 39 | func NewHealthChecker() *Health { 40 | return &Health{} 41 | } 42 | 43 | // IsUnknown returns true if Status is Unknown 44 | func (h *Health) IsUnknown() bool { 45 | return h.Status == ServiceUnknown 46 | } 47 | 48 | // IsUp returns true if Status is Up 49 | func (h *Health) IsUp() bool { 50 | return h.Status == ServiceUp 51 | } 52 | 53 | // IsDown returns true if Status is Down 54 | func (h *Health) IsDown() bool { 55 | return h.Status == ServiceDown 56 | } 57 | 58 | // Down set the Status to Down 59 | func (h *Health) Down() *Health { 60 | h.Status = ServiceDown 61 | return h 62 | } 63 | 64 | // Unknown set the Status to Unknown 65 | func (h *Health) Unknown() *Health { 66 | h.Status = ServiceUnknown 67 | return h 68 | } 69 | 70 | // Up set the Status to Up 71 | func (h *Health) Up() *Health { 72 | h.Status = ServiceUp 73 | return h 74 | } 75 | 76 | // ChecksStatus get checks Status 77 | func (h *Health) ChecksStatus() string { 78 | return h.Status 79 | } 80 | 81 | // ChecksReport get checks Status 82 | func (h *Health) ChecksReport() (string, error) { 83 | bytes, err := json.Marshal(h.Checks) 84 | if err != nil { 85 | return "", err 86 | } 87 | return string(bytes), nil 88 | } 89 | 90 | // AddCheck adds a new check 91 | func (h *Health) AddCheck(ID string, callable func() (bool, error)) { 92 | check := &Check{ 93 | ID: ID, 94 | Status: ServiceUnknown, 95 | callable: callable, 96 | } 97 | h.Checks = append(h.Checks, check) 98 | } 99 | 100 | // RunChecks runs all health checks 101 | func (h *Health) RunChecks() { 102 | upCount := 0 103 | downCount := 0 104 | var err error 105 | for _, check := range h.Checks { 106 | check.Result, err = check.callable() 107 | if err != nil { 108 | check.Error = err.Error() 109 | } 110 | if check.Result { 111 | check.Status = ServiceUp 112 | upCount++ 113 | } else { 114 | check.Status = ServiceDown 115 | downCount++ 116 | } 117 | } 118 | if downCount > 0 { 119 | h.Down() 120 | } else { 121 | h.Up() 122 | } 123 | } 124 | 125 | // HTTPCheck do HTTP health check 126 | func HTTPCheck(ctx context.Context, serviceName, URL string, parameters map[string]string, headers map[string]string) (bool, error) { 127 | httpClient := NewHTTPClient() 128 | response, error := httpClient.Get( 129 | ctx, 130 | URL, 131 | parameters, 132 | headers, 133 | ) 134 | 135 | if error != nil { 136 | return false, error 137 | } 138 | 139 | if httpClient.GetStatusCode(response) == http.StatusServiceUnavailable { 140 | return false, fmt.Errorf("Service %s is unavailable", serviceName) 141 | } 142 | 143 | return true, nil 144 | } 145 | 146 | // RedisCheck do a redis health check 147 | func RedisCheck(serviceName string, addr string, password string, db int) (bool, error) { 148 | redisDriver := NewRedisDriver(addr, password, db) 149 | _, err := redisDriver.Connect() 150 | 151 | if err != nil { 152 | return false, fmt.Errorf("Error while connecting %s: %s", serviceName, err.Error()) 153 | } 154 | 155 | status, err := redisDriver.Ping() 156 | 157 | if err != nil { 158 | return false, fmt.Errorf("Error while connecting %s: %s", serviceName, err.Error()) 159 | } 160 | 161 | return status, nil 162 | } 163 | -------------------------------------------------------------------------------- /caching.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Clivern. All rights reserved. 2 | // Use of this source code is governed by the MIT 3 | // license that can be found in the LICENSE file. 4 | 5 | package hippo 6 | 7 | import ( 8 | "github.com/go-redis/redis" 9 | "time" 10 | ) 11 | 12 | // Redis driver 13 | type Redis struct { 14 | Client *redis.Client 15 | Addr string 16 | Password string 17 | DB int 18 | } 19 | 20 | // Message item 21 | type Message struct { 22 | Channel string 23 | Payload string 24 | } 25 | 26 | // NewRedisDriver create a new instance 27 | func NewRedisDriver(addr string, password string, db int) *Redis { 28 | return &Redis{ 29 | Addr: addr, 30 | Password: password, 31 | DB: db, 32 | } 33 | } 34 | 35 | // Connect establish a redis connection 36 | func (r *Redis) Connect() (bool, error) { 37 | r.Client = redis.NewClient(&redis.Options{ 38 | Addr: r.Addr, 39 | Password: r.Password, 40 | DB: r.DB, 41 | }) 42 | 43 | _, err := r.Ping() 44 | 45 | if err != nil { 46 | return false, err 47 | } 48 | 49 | return true, nil 50 | } 51 | 52 | // Ping checks the redis connection 53 | func (r *Redis) Ping() (bool, error) { 54 | pong, err := r.Client.Ping().Result() 55 | 56 | if err != nil { 57 | return false, err 58 | } 59 | return pong == "PONG", nil 60 | } 61 | 62 | // Set sets a record 63 | func (r *Redis) Set(key, value string, expiration time.Duration) (bool, error) { 64 | result := r.Client.Set(key, value, expiration) 65 | 66 | if result.Err() != nil { 67 | return false, result.Err() 68 | } 69 | 70 | return result.Val() == "OK", nil 71 | } 72 | 73 | // Get gets a record value 74 | func (r *Redis) Get(key string) (string, error) { 75 | result := r.Client.Get(key) 76 | 77 | if result.Err() != nil { 78 | return "", result.Err() 79 | } 80 | 81 | return result.Val(), nil 82 | } 83 | 84 | // Exists deletes a record 85 | func (r *Redis) Exists(key string) (bool, error) { 86 | result := r.Client.Exists(key) 87 | 88 | if result.Err() != nil { 89 | return false, result.Err() 90 | } 91 | 92 | return result.Val() > 0, nil 93 | } 94 | 95 | // Del deletes a record 96 | func (r *Redis) Del(key string) (int64, error) { 97 | result := r.Client.Del(key) 98 | 99 | if result.Err() != nil { 100 | return 0, result.Err() 101 | } 102 | 103 | return result.Val(), nil 104 | } 105 | 106 | // HGet gets a record from hash 107 | func (r *Redis) HGet(key, field string) (string, error) { 108 | result := r.Client.HGet(key, field) 109 | 110 | if result.Err() != nil { 111 | return "", result.Err() 112 | } 113 | 114 | return result.Val(), nil 115 | } 116 | 117 | // HSet sets a record in hash 118 | func (r *Redis) HSet(key, field, value string) (bool, error) { 119 | result := r.Client.HSet(key, field, value) 120 | 121 | if result.Err() != nil { 122 | return false, result.Err() 123 | } 124 | 125 | return result.Val(), nil 126 | } 127 | 128 | // HExists checks if key exists on a hash 129 | func (r *Redis) HExists(key, field string) (bool, error) { 130 | result := r.Client.HExists(key, field) 131 | 132 | if result.Err() != nil { 133 | return false, result.Err() 134 | } 135 | 136 | return result.Val(), nil 137 | } 138 | 139 | // HDel deletes a hash record 140 | func (r *Redis) HDel(key, field string) (int64, error) { 141 | result := r.Client.HDel(key, field) 142 | 143 | if result.Err() != nil { 144 | return 0, result.Err() 145 | } 146 | 147 | return result.Val(), nil 148 | } 149 | 150 | // HLen count hash records 151 | func (r *Redis) HLen(key string) (int64, error) { 152 | result := r.Client.HLen(key) 153 | 154 | if result.Err() != nil { 155 | return 0, result.Err() 156 | } 157 | 158 | return result.Val(), nil 159 | } 160 | 161 | // HTruncate deletes a hash 162 | func (r *Redis) HTruncate(key string) (int64, error) { 163 | result := r.Client.Del(key) 164 | 165 | if result.Err() != nil { 166 | return 0, result.Err() 167 | } 168 | 169 | return result.Val(), nil 170 | } 171 | 172 | // HScan return an iterative obj for a hash 173 | func (r *Redis) HScan(key string, cursor uint64, match string, count int64) *redis.ScanCmd { 174 | return r.Client.HScan(key, cursor, match, count) 175 | } 176 | 177 | // Publish sends a message to channel 178 | func (r *Redis) Publish(channel string, message string) (bool, error) { 179 | result := r.Client.Publish(channel, message) 180 | 181 | if result.Err() != nil { 182 | return false, result.Err() 183 | } 184 | return true, nil 185 | } 186 | 187 | // Subscribe listens to a channel 188 | func (r *Redis) Subscribe(channel string, callback func(message Message) error) error { 189 | pubsub := r.Client.Subscribe(channel) 190 | defer pubsub.Close() 191 | 192 | ch := pubsub.Channel() 193 | 194 | for msg := range ch { 195 | message := Message{ 196 | Channel: msg.Channel, 197 | Payload: msg.Payload, 198 | } 199 | err := callback(message) 200 | if err != nil { 201 | return err 202 | } 203 | } 204 | return nil 205 | } 206 | -------------------------------------------------------------------------------- /time_series.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Clivern. All rights reserved. 2 | // Use of this source code is governed by the MIT 3 | // license that can be found in the LICENSE file. 4 | 5 | package hippo 6 | 7 | import ( 8 | "bytes" 9 | "fmt" 10 | "log" 11 | "net" 12 | "time" 13 | ) 14 | 15 | // defaultTimeout is the default number of seconds that we're willing to wait 16 | const defaultTimeout = 5 17 | 18 | // TimeSeries interface 19 | type TimeSeries interface { 20 | Connect() error 21 | Disconnect() error 22 | SendMetrics(metrics []Metric) error 23 | SendMetric(metric Metric) error 24 | IsNop() bool 25 | } 26 | 27 | // Metric struct 28 | type Metric struct { 29 | Name string 30 | Value string 31 | Timestamp int64 32 | } 33 | 34 | // graphiteClient struct 35 | type graphiteClient struct { 36 | Host string 37 | Port int 38 | Protocol string 39 | Timeout time.Duration 40 | Prefix string 41 | conn net.Conn 42 | nop bool 43 | } 44 | 45 | // String transfer the metric to string 46 | func (metric Metric) String() string { 47 | return fmt.Sprintf( 48 | "%s %s %s", 49 | metric.Name, 50 | metric.Value, 51 | time.Unix(metric.Timestamp, 0).Format("2006-01-02 15:04:05"), 52 | ) 53 | } 54 | 55 | // NewMetric creates a new metric 56 | func NewMetric(name, value string, timestamp int64) Metric { 57 | return Metric{ 58 | Name: name, 59 | Value: value, 60 | Timestamp: timestamp, 61 | } 62 | } 63 | 64 | // NewMetrics creates a new metrics array 65 | func NewMetrics(name, value string, timestamp int64) []Metric { 66 | var metrics []Metric 67 | 68 | metric := Metric{ 69 | Name: name, 70 | Value: value, 71 | Timestamp: timestamp, 72 | } 73 | metrics = append(metrics, metric) 74 | return metrics 75 | } 76 | 77 | // NewGraphite create instance of graphite 78 | func NewGraphite(protocol string, host string, port int, prefix string) TimeSeries { 79 | var graph *graphiteClient 80 | 81 | switch protocol { 82 | case "tcp": 83 | graph = &graphiteClient{Host: host, Port: port, Protocol: "tcp", Prefix: prefix} 84 | case "udp": 85 | graph = &graphiteClient{Host: host, Port: port, Protocol: "udp", Prefix: prefix} 86 | case "nop": 87 | graph = &graphiteClient{Host: host, Port: port, nop: true} 88 | } 89 | 90 | return graph 91 | } 92 | 93 | // Connect connect to graphite 94 | func (graphite *graphiteClient) Connect() error { 95 | if !graphite.IsNop() { 96 | if graphite.conn != nil { 97 | graphite.conn.Close() 98 | } 99 | 100 | address := fmt.Sprintf("%s:%d", graphite.Host, graphite.Port) 101 | 102 | if graphite.Timeout == 0 { 103 | graphite.Timeout = defaultTimeout * time.Second 104 | } 105 | 106 | var err error 107 | var conn net.Conn 108 | 109 | if graphite.Protocol == "udp" { 110 | udpAddr, err := net.ResolveUDPAddr("udp", address) 111 | if err != nil { 112 | return err 113 | } 114 | conn, err = net.DialUDP(graphite.Protocol, nil, udpAddr) 115 | } else { 116 | conn, err = net.DialTimeout(graphite.Protocol, address, graphite.Timeout) 117 | } 118 | 119 | if err != nil { 120 | return err 121 | } 122 | 123 | graphite.conn = conn 124 | } 125 | 126 | return nil 127 | } 128 | 129 | // SendMetric sends metric to graphite 130 | func (graphite *graphiteClient) SendMetric(metric Metric) error { 131 | metrics := make([]Metric, 1) 132 | metrics[0] = metric 133 | 134 | return graphite.SendMetrics(metrics) 135 | } 136 | 137 | // SendMetrics sends metrics to graphite 138 | func (graphite *graphiteClient) SendMetrics(metrics []Metric) error { 139 | if graphite.IsNop() { 140 | for _, metric := range metrics { 141 | log.Printf("Graphite: %s\n", metric) 142 | } 143 | return nil 144 | } 145 | zeroedMetric := Metric{} // ignore unintialized metrics 146 | buf := bytes.NewBufferString("") 147 | for _, metric := range metrics { 148 | if metric == zeroedMetric { 149 | continue // ignore unintialized metrics 150 | } 151 | if metric.Timestamp == 0 { 152 | metric.Timestamp = time.Now().Unix() 153 | } 154 | metricName := "" 155 | if graphite.Prefix != "" { 156 | metricName = fmt.Sprintf("%s.%s", graphite.Prefix, metric.Name) 157 | } else { 158 | metricName = metric.Name 159 | } 160 | if graphite.Protocol == "udp" { 161 | fmt.Fprintf(graphite.conn, "%s %s %d\n", metricName, metric.Value, metric.Timestamp) 162 | continue 163 | } 164 | buf.WriteString(fmt.Sprintf("%s %s %d\n", metricName, metric.Value, metric.Timestamp)) 165 | } 166 | if graphite.Protocol == "tcp" { 167 | _, err := graphite.conn.Write(buf.Bytes()) 168 | if err != nil { 169 | return err 170 | } 171 | } 172 | return nil 173 | } 174 | 175 | // IsNop is a getter for *graphite.Graphite.nop 176 | func (graphite *graphiteClient) IsNop() bool { 177 | if graphite.nop { 178 | return true 179 | } 180 | return false 181 | } 182 | 183 | // Disconnect disconnect the connection 184 | func (graphite *graphiteClient) Disconnect() error { 185 | err := graphite.conn.Close() 186 | graphite.conn = nil 187 | return err 188 | } 189 | -------------------------------------------------------------------------------- /service_discovery.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Clivern. All rights reserved. 2 | // Use of this source code is governed by the MIT 3 | // license that can be found in the LICENSE file. 4 | 5 | package hippo 6 | 7 | import ( 8 | "context" 9 | "fmt" 10 | "net/http" 11 | "strings" 12 | ) 13 | 14 | // ConsulConfig struct 15 | type ConsulConfig struct { 16 | URL string 17 | Version string 18 | } 19 | 20 | // ConsulStatus struct 21 | type ConsulStatus struct { 22 | Config ConsulConfig 23 | } 24 | 25 | // ConsulKv struct 26 | type ConsulKv struct { 27 | Config ConsulConfig 28 | } 29 | 30 | // GetRaftLeader returns the Raft leader for the datacenter in which the agent is running 31 | func (c *ConsulStatus) GetRaftLeader(ctx context.Context, parameters map[string]string) (string, error) { 32 | endpoint := fmt.Sprintf( 33 | "%s/%s/status/leader", 34 | strings.TrimSuffix(c.Config.URL, "/"), 35 | c.Config.Version, 36 | ) 37 | 38 | httpClient := NewHTTPClient() 39 | 40 | response, err := httpClient.Get(ctx, endpoint, parameters, map[string]string{}) 41 | 42 | if err != nil { 43 | return "", err 44 | } 45 | 46 | if httpClient.GetStatusCode(response) != http.StatusOK { 47 | return "", fmt.Errorf("Error: Invalid HTTP status code %d", httpClient.GetStatusCode(response)) 48 | } 49 | 50 | body, err := httpClient.ToString(response) 51 | 52 | if err != nil { 53 | return "", err 54 | } 55 | 56 | return body, nil 57 | } 58 | 59 | // ListRaftPeers retrieves the Raft peers for the datacenter in which the the agent is running 60 | func (c *ConsulStatus) ListRaftPeers(ctx context.Context, parameters map[string]string) (string, error) { 61 | endpoint := fmt.Sprintf( 62 | "%s/%s/status/peers", 63 | strings.TrimSuffix(c.Config.URL, "/"), 64 | c.Config.Version, 65 | ) 66 | 67 | httpClient := NewHTTPClient() 68 | 69 | response, err := httpClient.Get(ctx, endpoint, parameters, map[string]string{}) 70 | 71 | if err != nil { 72 | return "", err 73 | } 74 | 75 | if httpClient.GetStatusCode(response) != http.StatusOK { 76 | return "", fmt.Errorf("Error: Invalid HTTP status code %d", httpClient.GetStatusCode(response)) 77 | } 78 | 79 | body, err := httpClient.ToString(response) 80 | 81 | if err != nil { 82 | return "", err 83 | } 84 | 85 | return body, nil 86 | } 87 | 88 | // Read gets a kv 89 | func (c *ConsulKv) Read(ctx context.Context, key string, parameters map[string]string) (string, error) { 90 | endpoint := fmt.Sprintf( 91 | "%s/%s/kv/%s", 92 | strings.TrimSuffix(c.Config.URL, "/"), 93 | c.Config.Version, 94 | key, 95 | ) 96 | 97 | httpClient := NewHTTPClient() 98 | 99 | response, err := httpClient.Get(ctx, endpoint, parameters, map[string]string{}) 100 | 101 | if err != nil { 102 | return "", err 103 | } 104 | 105 | if httpClient.GetStatusCode(response) == http.StatusNotFound { 106 | return "", fmt.Errorf("Error: Key [%s] does not exist", key) 107 | } 108 | 109 | if httpClient.GetStatusCode(response) != http.StatusOK { 110 | return "", fmt.Errorf("Error: Invalid HTTP status code %d", httpClient.GetStatusCode(response)) 111 | } 112 | 113 | body, err := httpClient.ToString(response) 114 | 115 | if err != nil { 116 | return "", err 117 | } 118 | 119 | return body, nil 120 | } 121 | 122 | // Update update or create a kv 123 | func (c *ConsulKv) Update(ctx context.Context, key string, value string, parameters map[string]string) (string, error) { 124 | endpoint := fmt.Sprintf( 125 | "%s/%s/kv/%s", 126 | strings.TrimSuffix(c.Config.URL, "/"), 127 | c.Config.Version, 128 | key, 129 | ) 130 | 131 | httpClient := NewHTTPClient() 132 | 133 | response, err := httpClient.Post(ctx, endpoint, value, parameters, map[string]string{}) 134 | 135 | if err != nil { 136 | return "", err 137 | } 138 | 139 | if httpClient.GetStatusCode(response) != http.StatusOK { 140 | return "", fmt.Errorf("Error: Invalid HTTP status code %d", httpClient.GetStatusCode(response)) 141 | } 142 | 143 | body, err := httpClient.ToString(response) 144 | 145 | if err != nil { 146 | return "", err 147 | } 148 | 149 | return body, nil 150 | } 151 | 152 | // Delete deletes a kv 153 | func (c *ConsulKv) Delete(ctx context.Context, key string, parameters map[string]string) (string, error) { 154 | endpoint := fmt.Sprintf( 155 | "%s/%s/kv/%s", 156 | strings.TrimSuffix(c.Config.URL, "/"), 157 | c.Config.Version, 158 | key, 159 | ) 160 | 161 | httpClient := NewHTTPClient() 162 | 163 | response, err := httpClient.Get(ctx, endpoint, parameters, map[string]string{}) 164 | 165 | if err != nil { 166 | return "", err 167 | } 168 | 169 | if httpClient.GetStatusCode(response) == http.StatusNotFound { 170 | return "", fmt.Errorf("Error: Key [%s] does not exist", key) 171 | } 172 | 173 | if httpClient.GetStatusCode(response) != http.StatusOK { 174 | return "", fmt.Errorf("Error: Invalid HTTP status code %d", httpClient.GetStatusCode(response)) 175 | } 176 | 177 | body, err := httpClient.ToString(response) 178 | 179 | if err != nil { 180 | return "", err 181 | } 182 | 183 | return body, nil 184 | } 185 | -------------------------------------------------------------------------------- /service_discovery_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Clivern. All rights reserved. 2 | // Use of this source code is governed by the MIT 3 | // license that can be found in the LICENSE file. 4 | 5 | package hippo 6 | 7 | import ( 8 | "context" 9 | "fmt" 10 | "github.com/nbio/st" 11 | "io/ioutil" 12 | "net/http" 13 | "net/http/httptest" 14 | "testing" 15 | ) 16 | 17 | // TestStatusGetRaftLeader test cases 18 | func TestStatusGetRaftLeader(t *testing.T) { 19 | version := "v1" 20 | endpoint := "status/leader" 21 | 22 | // Mock http server 23 | ts := httptest.NewServer( 24 | http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 25 | if r.URL.Path == fmt.Sprintf("/%s/%s", version, endpoint) { 26 | w.WriteHeader(http.StatusOK) 27 | w.Header().Add("Content-Type", "application/json") 28 | w.Write([]byte(`true`)) 29 | } 30 | }), 31 | ) 32 | defer ts.Close() 33 | 34 | t.Log(fmt.Sprintf("%s/%s/%s", ts.URL, version, endpoint)) 35 | 36 | config := ConsulConfig{ 37 | URL: ts.URL, 38 | Version: version, 39 | } 40 | 41 | status := ConsulStatus{ 42 | Config: config, 43 | } 44 | 45 | body, error := status.GetRaftLeader(context.Background(), map[string]string{}) 46 | 47 | t.Log(body) 48 | t.Log(error) 49 | st.Expect(t, "true", body) 50 | st.Expect(t, nil, error) 51 | } 52 | 53 | // TestStatusListRaftPeers test cases 54 | func TestStatusListRaftPeers(t *testing.T) { 55 | version := "v1" 56 | endpoint := "status/peers" 57 | 58 | // Mock http server 59 | ts := httptest.NewServer( 60 | http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 61 | if r.URL.Path == fmt.Sprintf("/%s/%s", version, endpoint) { 62 | w.WriteHeader(http.StatusOK) 63 | w.Header().Add("Content-Type", "application/json") 64 | w.Write([]byte(`true`)) 65 | } 66 | }), 67 | ) 68 | defer ts.Close() 69 | 70 | t.Log(fmt.Sprintf("%s/%s/%s", ts.URL, version, endpoint)) 71 | 72 | config := ConsulConfig{ 73 | URL: ts.URL, 74 | Version: version, 75 | } 76 | 77 | status := ConsulStatus{ 78 | Config: config, 79 | } 80 | 81 | body, error := status.ListRaftPeers(context.Background(), map[string]string{}) 82 | 83 | t.Log(body) 84 | t.Log(error) 85 | st.Expect(t, "true", body) 86 | st.Expect(t, nil, error) 87 | } 88 | 89 | // TestKvRead test cases 90 | func TestKvRead(t *testing.T) { 91 | version := "v1" 92 | endpoint := "kv" 93 | item := "key" 94 | 95 | // Mock http server 96 | ts := httptest.NewServer( 97 | http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 98 | if r.URL.Path == fmt.Sprintf("/%s/%s/%s", version, endpoint, item) { 99 | w.WriteHeader(http.StatusOK) 100 | w.Header().Add("Content-Type", "application/json") 101 | w.Write([]byte(`true`)) 102 | } 103 | }), 104 | ) 105 | defer ts.Close() 106 | 107 | t.Log(fmt.Sprintf("%s/%s/%s/%s", ts.URL, version, endpoint, item)) 108 | 109 | config := ConsulConfig{ 110 | URL: ts.URL, 111 | Version: version, 112 | } 113 | 114 | kv := ConsulKv{ 115 | Config: config, 116 | } 117 | 118 | body, error := kv.Read(context.Background(), "key", map[string]string{}) 119 | 120 | t.Log(body) 121 | t.Log(error) 122 | st.Expect(t, "true", body) 123 | st.Expect(t, nil, error) 124 | } 125 | 126 | // TestKvUpdate test cases 127 | func TestKvUpdate(t *testing.T) { 128 | version := "v1" 129 | endpoint := "kv" 130 | item := "key" 131 | 132 | // Mock http server 133 | ts := httptest.NewServer( 134 | http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 135 | if r.URL.Path == fmt.Sprintf("/%s/%s/%s", version, endpoint, item) { 136 | w.WriteHeader(http.StatusOK) 137 | w.Header().Add("Content-Type", "application/json") 138 | body, _ := ioutil.ReadAll(r.Body) 139 | w.Write([]byte(string(body))) 140 | } 141 | }), 142 | ) 143 | defer ts.Close() 144 | 145 | t.Log(fmt.Sprintf("%s/%s/%s/%s", ts.URL, version, endpoint, item)) 146 | 147 | config := ConsulConfig{ 148 | URL: ts.URL, 149 | Version: version, 150 | } 151 | 152 | kv := ConsulKv{ 153 | Config: config, 154 | } 155 | 156 | body, error := kv.Update(context.Background(), "key", "value", map[string]string{}) 157 | 158 | t.Log(body) 159 | t.Log(error) 160 | st.Expect(t, "value", body) 161 | st.Expect(t, nil, error) 162 | } 163 | 164 | // TestKvDelete test cases 165 | func TestKvDelete(t *testing.T) { 166 | version := "v1" 167 | endpoint := "kv" 168 | item := "key" 169 | 170 | // Mock http server 171 | ts := httptest.NewServer( 172 | http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 173 | if r.URL.Path == fmt.Sprintf("/%s/%s/%s", version, endpoint, item) { 174 | w.WriteHeader(http.StatusOK) 175 | w.Header().Add("Content-Type", "application/json") 176 | w.Write([]byte(`true`)) 177 | } 178 | }), 179 | ) 180 | defer ts.Close() 181 | 182 | t.Log(fmt.Sprintf("%s/%s/%s/%s", ts.URL, version, endpoint, item)) 183 | 184 | config := ConsulConfig{ 185 | URL: ts.URL, 186 | Version: version, 187 | } 188 | 189 | kv := ConsulKv{ 190 | Config: config, 191 | } 192 | 193 | body, error := kv.Delete(context.Background(), "key", map[string]string{}) 194 | 195 | t.Log(body) 196 | t.Log(error) 197 | st.Expect(t, "true", body) 198 | st.Expect(t, nil, error) 199 | } 200 | -------------------------------------------------------------------------------- /http.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Clivern. All rights reserved. 2 | // Use of this source code is governed by the MIT 3 | // license that can be found in the LICENSE file. 4 | 5 | package hippo 6 | 7 | import ( 8 | "bytes" 9 | "context" 10 | "fmt" 11 | "io/ioutil" 12 | "net/http" 13 | "net/url" 14 | "strings" 15 | ) 16 | 17 | // HTTPClient interface 18 | type HTTPClient interface { 19 | Get(ctx context.Context, endpoint string, parameters map[string]string, headers map[string]string) (*http.Response, error) 20 | Post(ctx context.Context, endpoint string, data string, parameters map[string]string, headers map[string]string) (*http.Response, error) 21 | Put(ctx context.Context, endpoint string, data string, parameters map[string]string, headers map[string]string) (*http.Response, error) 22 | Delete(ctx context.Context, endpoint string, parameters map[string]string, headers map[string]string) (*http.Response, error) 23 | BuildParameters(endpoint string, parameters map[string]string) (string, error) 24 | ToString(response *http.Response) (string, error) 25 | BuildData(parameters map[string]string) string 26 | GetStatusCode(response *http.Response) int 27 | } 28 | 29 | // httpClient struct 30 | type httpClient struct { 31 | } 32 | 33 | // NewHTTPClient creates an instance of http client 34 | func NewHTTPClient() HTTPClient { 35 | httpClient := &httpClient{} 36 | return httpClient 37 | } 38 | 39 | // Get http call 40 | func (h *httpClient) Get(ctx context.Context, endpoint string, parameters map[string]string, headers map[string]string) (*http.Response, error) { 41 | 42 | endpoint, err := h.BuildParameters(endpoint, parameters) 43 | 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | req, _ := http.NewRequest("GET", endpoint, nil) 49 | 50 | req = req.WithContext(ctx) 51 | 52 | for k, v := range headers { 53 | req.Header.Add(k, v) 54 | } 55 | 56 | client := http.Client{} 57 | 58 | resp, err := client.Do(req) 59 | 60 | if err != nil { 61 | return resp, err 62 | } 63 | 64 | return resp, err 65 | } 66 | 67 | // Post http call 68 | func (h *httpClient) Post(ctx context.Context, endpoint string, data string, parameters map[string]string, headers map[string]string) (*http.Response, error) { 69 | 70 | endpoint, err := h.BuildParameters(endpoint, parameters) 71 | 72 | if err != nil { 73 | return nil, err 74 | } 75 | 76 | req, _ := http.NewRequest("POST", endpoint, bytes.NewBuffer([]byte(data))) 77 | 78 | req = req.WithContext(ctx) 79 | 80 | for k, v := range headers { 81 | req.Header.Add(k, v) 82 | } 83 | 84 | client := http.Client{} 85 | 86 | resp, err := client.Do(req) 87 | 88 | if err != nil { 89 | return resp, err 90 | } 91 | 92 | return resp, err 93 | } 94 | 95 | // Put http call 96 | func (h *httpClient) Put(ctx context.Context, endpoint string, data string, parameters map[string]string, headers map[string]string) (*http.Response, error) { 97 | 98 | endpoint, err := h.BuildParameters(endpoint, parameters) 99 | 100 | if err != nil { 101 | return nil, err 102 | } 103 | 104 | req, _ := http.NewRequest("PUT", endpoint, bytes.NewBuffer([]byte(data))) 105 | 106 | req = req.WithContext(ctx) 107 | 108 | for k, v := range headers { 109 | req.Header.Add(k, v) 110 | } 111 | 112 | client := http.Client{} 113 | 114 | resp, err := client.Do(req) 115 | 116 | if err != nil { 117 | return resp, err 118 | } 119 | 120 | return resp, err 121 | } 122 | 123 | // Delete http call 124 | func (h *httpClient) Delete(ctx context.Context, endpoint string, parameters map[string]string, headers map[string]string) (*http.Response, error) { 125 | 126 | endpoint, err := h.BuildParameters(endpoint, parameters) 127 | 128 | if err != nil { 129 | return nil, err 130 | } 131 | 132 | req, _ := http.NewRequest("DELETE", endpoint, nil) 133 | 134 | req = req.WithContext(ctx) 135 | 136 | for k, v := range headers { 137 | req.Header.Add(k, v) 138 | } 139 | 140 | client := http.Client{} 141 | 142 | resp, err := client.Do(req) 143 | 144 | if err != nil { 145 | return resp, err 146 | } 147 | 148 | return resp, err 149 | } 150 | 151 | // BuildParameters add parameters to URL 152 | func (h *httpClient) BuildParameters(endpoint string, parameters map[string]string) (string, error) { 153 | u, err := url.Parse(endpoint) 154 | 155 | if err != nil { 156 | return "", err 157 | } 158 | 159 | q := u.Query() 160 | 161 | for k, v := range parameters { 162 | q.Set(k, v) 163 | } 164 | u.RawQuery = q.Encode() 165 | 166 | return u.String(), nil 167 | } 168 | 169 | // BuildData build body data 170 | func (h *httpClient) BuildData(parameters map[string]string) string { 171 | var items []string 172 | 173 | for k, v := range parameters { 174 | items = append(items, fmt.Sprintf("%s=%s", k, v)) 175 | } 176 | 177 | return strings.Join(items, "&") 178 | } 179 | 180 | // ToString response body to string 181 | func (h *httpClient) ToString(response *http.Response) (string, error) { 182 | defer response.Body.Close() 183 | 184 | body, err := ioutil.ReadAll(response.Body) 185 | 186 | if err != nil { 187 | return "", err 188 | } 189 | 190 | return string(body), nil 191 | } 192 | 193 | // GetStatusCode response status code 194 | func (h *httpClient) GetStatusCode(response *http.Response) int { 195 | return response.StatusCode 196 | } 197 | -------------------------------------------------------------------------------- /http_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Clivern. All rights reserved. 2 | // Use of this source code is governed by the MIT 3 | // license that can be found in the LICENSE file. 4 | 5 | package hippo 6 | 7 | import ( 8 | "context" 9 | "net/http" 10 | "strings" 11 | "testing" 12 | 13 | "github.com/nbio/st" 14 | ) 15 | 16 | // TestHttpGet test cases 17 | func TestHttpGet(t *testing.T) { 18 | httpClient := NewHTTPClient() 19 | response, error := httpClient.Get( 20 | context.Background(), 21 | "https://httpbin.org/get", 22 | map[string]string{"arg1": "value1"}, 23 | map[string]string{"X-Auth": "hipp-123"}, 24 | ) 25 | t.Log(httpClient.GetStatusCode(response)) 26 | st.Expect(t, http.StatusOK, httpClient.GetStatusCode(response)) 27 | st.Expect(t, nil, error) 28 | 29 | body, error := httpClient.ToString(response) 30 | t.Log(body) 31 | st.Expect(t, true, strings.Contains(body, "value1")) 32 | st.Expect(t, true, strings.Contains(body, "arg1")) 33 | st.Expect(t, true, strings.Contains(body, "arg1=value1")) 34 | st.Expect(t, true, strings.Contains(body, "X-Auth")) 35 | st.Expect(t, true, strings.Contains(body, "hipp-123")) 36 | st.Expect(t, nil, error) 37 | } 38 | 39 | // TestHttpDelete test cases 40 | func TestHttpDelete(t *testing.T) { 41 | httpClient := NewHTTPClient() 42 | response, error := httpClient.Delete( 43 | context.Background(), 44 | "https://httpbin.org/delete", 45 | map[string]string{"arg1": "value1"}, 46 | map[string]string{"X-Auth": "hipp-123"}, 47 | ) 48 | t.Log(httpClient.GetStatusCode(response)) 49 | st.Expect(t, http.StatusOK, httpClient.GetStatusCode(response)) 50 | st.Expect(t, nil, error) 51 | 52 | body, error := httpClient.ToString(response) 53 | t.Log(body) 54 | st.Expect(t, true, strings.Contains(body, "value1")) 55 | st.Expect(t, true, strings.Contains(body, "arg1")) 56 | st.Expect(t, true, strings.Contains(body, "arg1=value1")) 57 | st.Expect(t, true, strings.Contains(body, "X-Auth")) 58 | st.Expect(t, true, strings.Contains(body, "hipp-123")) 59 | st.Expect(t, nil, error) 60 | } 61 | 62 | // TestHttpPost test cases 63 | func TestHttpPost(t *testing.T) { 64 | httpClient := NewHTTPClient() 65 | response, error := httpClient.Post( 66 | context.Background(), 67 | "https://httpbin.org/post", 68 | `{"Username":"admin", "Password":"12345"}`, 69 | map[string]string{"arg1": "value1"}, 70 | map[string]string{"X-Auth": "hipp-123"}, 71 | ) 72 | t.Log(httpClient.GetStatusCode(response)) 73 | st.Expect(t, http.StatusOK, httpClient.GetStatusCode(response)) 74 | st.Expect(t, nil, error) 75 | 76 | body, error := httpClient.ToString(response) 77 | t.Log(body) 78 | st.Expect(t, true, strings.Contains(body, `"12345"`)) 79 | st.Expect(t, true, strings.Contains(body, `"Username"`)) 80 | st.Expect(t, true, strings.Contains(body, `"admin"`)) 81 | st.Expect(t, true, strings.Contains(body, `"Password"`)) 82 | st.Expect(t, true, strings.Contains(body, "value1")) 83 | st.Expect(t, true, strings.Contains(body, "arg1")) 84 | st.Expect(t, true, strings.Contains(body, "arg1=value1")) 85 | st.Expect(t, true, strings.Contains(body, "X-Auth")) 86 | st.Expect(t, true, strings.Contains(body, "hipp-123")) 87 | st.Expect(t, nil, error) 88 | } 89 | 90 | // TestHttpPut test cases 91 | func TestHttpPut(t *testing.T) { 92 | httpClient := NewHTTPClient() 93 | response, error := httpClient.Put( 94 | context.Background(), 95 | "https://httpbin.org/put", 96 | `{"Username":"admin", "Password":"12345"}`, 97 | map[string]string{"arg1": "value1"}, 98 | map[string]string{"X-Auth": "hipp-123"}, 99 | ) 100 | t.Log(httpClient.GetStatusCode(response)) 101 | st.Expect(t, http.StatusOK, httpClient.GetStatusCode(response)) 102 | st.Expect(t, nil, error) 103 | 104 | body, error := httpClient.ToString(response) 105 | t.Log(body) 106 | st.Expect(t, true, strings.Contains(body, `"12345"`)) 107 | st.Expect(t, true, strings.Contains(body, `"Username"`)) 108 | st.Expect(t, true, strings.Contains(body, `"admin"`)) 109 | st.Expect(t, true, strings.Contains(body, `"Password"`)) 110 | st.Expect(t, true, strings.Contains(body, "value1")) 111 | st.Expect(t, true, strings.Contains(body, "arg1")) 112 | st.Expect(t, true, strings.Contains(body, "arg1=value1")) 113 | st.Expect(t, true, strings.Contains(body, "X-Auth")) 114 | st.Expect(t, true, strings.Contains(body, "hipp-123")) 115 | st.Expect(t, nil, error) 116 | } 117 | 118 | // TestHttpGetStatusCode1 test cases 119 | func TestHttpGetStatusCode1(t *testing.T) { 120 | httpClient := NewHTTPClient() 121 | response, error := httpClient.Get( 122 | context.Background(), 123 | "https://httpbin.org/status/200", 124 | map[string]string{"arg1": "value1"}, 125 | map[string]string{"X-Auth": "hipp-123"}, 126 | ) 127 | t.Log(httpClient.GetStatusCode(response)) 128 | st.Expect(t, http.StatusOK, httpClient.GetStatusCode(response)) 129 | st.Expect(t, nil, error) 130 | 131 | body, error := httpClient.ToString(response) 132 | t.Log(body) 133 | st.Expect(t, "", body) 134 | st.Expect(t, nil, error) 135 | } 136 | 137 | // TestHttpGetStatusCode2 test cases 138 | func TestHttpGetStatusCode2(t *testing.T) { 139 | httpClient := NewHTTPClient() 140 | response, error := httpClient.Get( 141 | context.Background(), 142 | "https://httpbin.org/status/500", 143 | map[string]string{"arg1": "value1"}, 144 | map[string]string{"X-Auth": "hipp-123"}, 145 | ) 146 | t.Log(httpClient.GetStatusCode(response)) 147 | st.Expect(t, http.StatusInternalServerError, httpClient.GetStatusCode(response)) 148 | st.Expect(t, nil, error) 149 | 150 | body, error := httpClient.ToString(response) 151 | t.Log(body) 152 | st.Expect(t, "", body) 153 | st.Expect(t, nil, error) 154 | } 155 | 156 | // TestHttpGetStatusCode3 test cases 157 | func TestHttpGetStatusCode3(t *testing.T) { 158 | httpClient := NewHTTPClient() 159 | response, error := httpClient.Get( 160 | context.Background(), 161 | "https://httpbin.org/status/404", 162 | map[string]string{"arg1": "value1"}, 163 | map[string]string{"X-Auth": "hipp-123"}, 164 | ) 165 | t.Log(httpClient.GetStatusCode(response)) 166 | st.Expect(t, http.StatusNotFound, httpClient.GetStatusCode(response)) 167 | st.Expect(t, nil, error) 168 | 169 | body, error := httpClient.ToString(response) 170 | t.Log(body) 171 | st.Expect(t, "", body) 172 | st.Expect(t, nil, error) 173 | } 174 | 175 | // TestHttpGetStatusCode4 test cases 176 | func TestHttpGetStatusCode4(t *testing.T) { 177 | httpClient := NewHTTPClient() 178 | response, error := httpClient.Get( 179 | context.Background(), 180 | "https://httpbin.org/status/201", 181 | map[string]string{"arg1": "value1"}, 182 | map[string]string{"X-Auth": "hipp-123"}, 183 | ) 184 | t.Log(httpClient.GetStatusCode(response)) 185 | st.Expect(t, http.StatusCreated, httpClient.GetStatusCode(response)) 186 | st.Expect(t, nil, error) 187 | 188 | body, error := httpClient.ToString(response) 189 | t.Log(body) 190 | st.Expect(t, "", body) 191 | st.Expect(t, nil, error) 192 | } 193 | 194 | // TestBuildParameters test cases 195 | func TestBuildParameters(t *testing.T) { 196 | httpClient := NewHTTPClient() 197 | url, error := httpClient.BuildParameters("http://127.0.0.1", map[string]string{"arg1": "value1"}) 198 | t.Log(url) 199 | st.Expect(t, "http://127.0.0.1?arg1=value1", url) 200 | st.Expect(t, nil, error) 201 | } 202 | 203 | // TestBuildData test cases 204 | func TestBuildData(t *testing.T) { 205 | httpClient := NewHTTPClient() 206 | st.Expect(t, httpClient.BuildData(map[string]string{}), "") 207 | st.Expect(t, httpClient.BuildData(map[string]string{"arg1": "value1"}), "arg1=value1") 208 | } 209 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Hippo Logo 3 |

Hippo

4 |

A Microservices Toolkit.

5 |

6 | 7 | 8 | 9 | 10 | 11 |

12 |

13 | 14 | Hippo is a collection of well crafted go packages that help you build robust, reliable, maintainable microservices. It is not a full-fledged framework with lot of magic, predefined architecture, specific patterns and bullshit opinions so you will be the one behind the wheel. 15 | 16 | It provides libraries to implement components for service discovery, async jobs, authentication, authorization, logging, caching, metrics, tracing, rate-limiting...etc which are essential requirements for running microservices in production. 17 | 18 | ## Documentation 19 | 20 | ### Installation: 21 | 22 | ```golang 23 | go get -u github.com/clivern/hippo 24 | ``` 25 | ```golang 26 | import ( 27 | "github.com/clivern/hippo" 28 | ) 29 | ``` 30 | 31 | ### Components: 32 | 33 | **HTTP Requests Component** 34 | 35 | ```golang 36 | 37 | httpClient := hippo.NewHTTPClient() 38 | 39 | // Get Request 40 | response, err := httpClient.Get( 41 | "https://httpbin.org/get", 42 | map[string]string{"url_arg_key": "url_arg_value"}, 43 | map[string]string{"header_key": "header_value"}, 44 | ) 45 | 46 | // Delete Request 47 | response, err := httpClient.Delete( 48 | "https://httpbin.org/delete", 49 | map[string]string{"url_arg_key": "url_arg_value"}, 50 | map[string]string{"header_key": "header_value"}, 51 | ) 52 | 53 | // Post Request 54 | response, err := httpClient.Post( 55 | "https://httpbin.org/post", 56 | `{"RequestBodyKey":"RequestBodyValue"}`, 57 | map[string]string{"url_arg_key": "url_arg_value"}, 58 | map[string]string{"header_key": "header_value"}, 59 | ) 60 | 61 | // Put Request 62 | response, err := httpClient.Put( 63 | "https://httpbin.org/put", 64 | `{"RequestBodyKey":"RequestBodyValue"}`, 65 | map[string]string{"url_arg_key": "url_arg_value"}, 66 | map[string]string{"header_key": "header_value"}, 67 | ) 68 | 69 | // .... 70 | 71 | statusCode := httpClient.GetStatusCode(response) 72 | responseBody, err := httpClient.ToString(response) 73 | ``` 74 | 75 | **Cache/Redis Component** 76 | 77 | ```golang 78 | driver := hippo.NewRedisDriver("localhost:6379", "password", 0) 79 | 80 | // connect to redis server 81 | ok, err := driver.Connect() 82 | // ping check 83 | ok, err = driver.Ping() 84 | 85 | // set an item 86 | ok, err = driver.Set("app_name", "Hippo", 0) 87 | // check if exists 88 | ok, err = driver.Exists("app_name") 89 | // get value 90 | value, err := driver.Get("app_name") 91 | // delete an item 92 | count, err := driver.Del("app_name") 93 | 94 | // hash set 95 | ok, err = driver.HSet("configs", "app_name", "Hippo") 96 | // check if item on a hash 97 | ok, err = driver.HExists("configs", "app_name") 98 | // get item from a hash 99 | value, err = driver.HGet("configs", "app_name") 100 | // hash length 101 | count, err = driver.HLen("configs") 102 | // delete item from a hash 103 | count, err = driver.HDel("configs", "app_name") 104 | // clear the hash 105 | count, err = driver.HTruncate("configs") 106 | 107 | // Pub/Sub 108 | driver.Publish("hippo", "Hello") 109 | driver.Subscribe("hippo", func(message hippo.Message) error { 110 | // message.Channel 111 | // message.Payload 112 | return nil 113 | }) 114 | ``` 115 | 116 | **Time Series/Graphite Component** 117 | 118 | ```golang 119 | import "time" 120 | 121 | 122 | metric := hippo.NewMetric("hippo1.up", "23", time.Now().Unix()) // Type is hippo.Metric 123 | 124 | metrics := hippo.NewMetrics("hippo2.up", "35", time.Now().Unix()) // type is []hippo.Metric 125 | metrics = append(metrics, hippo.NewMetric("hippo2.down", "40", time.Now().Unix())) 126 | metrics = append(metrics, hippo.NewMetric("hippo2.error", "70", time.Now().Unix())) 127 | 128 | // NewGraphite(protocol string, host string, port int, prefix string) 129 | // protocol can be tcp, udp or nop 130 | // prefix is a metric prefix 131 | graphite := hippo.NewGraphite("tcp", "127.0.0.1", 2003, "") 132 | error := graphite.Connect() 133 | 134 | if error == nil{ 135 | // send one by one 136 | graphite.SendMetric(metric) 137 | 138 | // bulk send 139 | graphite.SendMetrics(metrics) 140 | } 141 | ```` 142 | 143 | **System Stats Component** 144 | 145 | ```golang 146 | // func NewSystemStats(enableCPU, enableMem, enableGC bool) *SystemStats { 147 | stats := hippo.NewSystemStats(true, true, true) 148 | stats.GetStats() // type map[string]uint64 149 | // map[cpu.cgo_calls:0 cpu.goroutines:1 mem.alloc:0....] 150 | ``` 151 | 152 | **Correlation ID Component** 153 | 154 | ```golang 155 | correlation := hippo.NewCorrelation() 156 | correlation.UUIDv4() 157 | ``` 158 | 159 | **Workers Pool Component** 160 | 161 | ```golang 162 | import "fmt" 163 | 164 | tasks := []*hippo.Task{ 165 | hippo.NewTask(func() (string, error) { 166 | fmt.Println("Task #1") 167 | return "Result 1", nil 168 | }), 169 | hippo.NewTask(func() (string, error) { 170 | fmt.Println("Task #2") 171 | return "Result 2", nil 172 | }), 173 | hippo.NewTask(func() (string, error) { 174 | fmt.Println("Task #3") 175 | return "Result 3", nil 176 | }), 177 | } 178 | 179 | // hippo.NewWorkersPool(tasks []*Task, concurrency int) *WorkersPool 180 | p := hippo.NewWorkersPool(tasks, 2) 181 | p.Run() 182 | 183 | var numErrors int 184 | for _, task := range p.Tasks { 185 | if task.Err != nil { 186 | fmt.Println(task.Err) 187 | numErrors++ 188 | } else { 189 | fmt.Println(task.Result) 190 | } 191 | if numErrors >= 10 { 192 | fmt.Println("Too many errors.") 193 | break 194 | } 195 | } 196 | ```` 197 | 198 | **Health Checker Component** 199 | 200 | ```golang 201 | import "fmt" 202 | 203 | healthChecker := hippo.NewHealthChecker() 204 | healthChecker.AddCheck("ping_check", func() (bool, error){ 205 | return true, nil 206 | }) 207 | healthChecker.AddCheck("db_check", func() (bool, error){ 208 | return false, fmt.Errorf("Database Down") 209 | }) 210 | healthChecker.RunChecks() 211 | 212 | fmt.Println(healthChecker.ChecksStatus()) 213 | // Output -> DOWN 214 | fmt.Println(healthChecker.ChecksReport()) 215 | // Output -> [{"id":"ping_check","status":"UP","error":"","result":true},{"id":"db_check","status":"DOWN","error":"Database Down","result":false}] 216 | ``` 217 | ```golang 218 | import "fmt" 219 | 220 | healthChecker := hippo.NewHealthChecker() 221 | 222 | healthChecker.AddCheck("url_check", func() (bool, error){ 223 | return hippo.HTTPCheck("httpbin_service", "https://httpbin.org/status/503", map[string]string{}, map[string]string{}) 224 | }) 225 | healthChecker.AddCheck("redis_check", func() (bool, error){ 226 | return hippo.RedisCheck("redis_service", "localhost:6379", "", 0) 227 | }) 228 | healthChecker.RunChecks() 229 | 230 | fmt.Println(healthChecker.ChecksStatus()) 231 | // Outputs -> DOWN 232 | fmt.Println(healthChecker.ChecksReport()) 233 | // Outputs -> [{"id":"url_check","status":"DOWN","error":"Service httpbin_service is unavailable","result":false},{"id":"redis_check","status":"DOWN","error":"Error while connecting redis_service: dial tcp [::1]:6379: connect: connection refused","result":false}] 234 | ``` 235 | 236 | **API Rate Limiting** 237 | 238 | ```golang 239 | 240 | import "time" 241 | 242 | // Create a limiter with a specific identifier(IP address or access token or username....etc) 243 | // NewCallerLimiter(identifier string, eventsRate rate.Limit, tokenBurst int) *rate.Limiter 244 | limiter := hippo.NewCallerLimiter("10.10.10.10", 100, 1) 245 | if limiter.Allow() == false { 246 | // Don't allow access 247 | } else { 248 | // Allow Access 249 | } 250 | 251 | 252 | // auto clean old clients (should run as background process) 253 | // CleanupCallers(cleanAfter time.Duration) 254 | go func(){ 255 | for { 256 | time.Sleep(60 * time.Second) 257 | hippo.CleanupCallers(60) 258 | } 259 | }() 260 | ``` 261 | 262 | **Logger Component** 263 | 264 | ```golang 265 | logger, _ := hippo.NewLogger("debug", "json", []string{"stdout", "/var/log/error.log"}) 266 | 267 | logger.Info("Hello World!") 268 | logger.Debug("Hello World!") 269 | logger.Warn("Hello World!") 270 | logger.Error("Hello World!") 271 | 272 | defer logger.Sync() 273 | 274 | // check if path exists 275 | exists := hippo.PathExists("/var/log") 276 | 277 | // check if file exists 278 | exists := hippo.FileExists("/var/log/error.log") 279 | 280 | // check if dir exists 281 | exists := hippo.DirExists("/var/log") 282 | 283 | // ensure that dir exists 284 | exists, err := hippo.EnsureDir("/var/log", 755) 285 | ``` 286 | 287 | **Latency Tracker Component** 288 | 289 | ```golang 290 | httpClient := hippo.NewHTTPClient() 291 | 292 | latency := hippo.NewLatencyTracker() 293 | latency.NewAction("api.call") 294 | 295 | // First HTTP Call 296 | start := time.Now() 297 | httpClient.Get( 298 | "https://httpbin.org/get", 299 | map[string]string{}, 300 | map[string]string{}, 301 | ) 302 | latency.SetPoint("api.call", start, time.Now()) 303 | 304 | // Another HTTP Call 305 | latency.SetStart("api.call", time.Now()) 306 | httpClient.Get( 307 | "https://httpbin.org/get", 308 | map[string]string{}, 309 | map[string]string{}, 310 | ) 311 | latency.SetEnd("api.call", time.Now()) 312 | 313 | // Now it will calculate the average 314 | fmt.Println(latency.GetLatency("api.call")) 315 | // Output 486.217112ms 316 | ``` 317 | 318 | 319 | ## Versioning 320 | 321 | For transparency into our release cycle and in striving to maintain backward compatibility, Hippo is maintained under the [Semantic Versioning guidelines](https://semver.org/) and release process is predictable and business-friendly. 322 | 323 | See the [Releases section of our GitHub project](https://github.com/clivern/hippo/releases) for changelogs for each release version of Hippo. It contains summaries of the most noteworthy changes made in each release. 324 | 325 | 326 | ## Bug tracker 327 | 328 | If you have any suggestions, bug reports, or annoyances please report them to our issue tracker at https://github.com/clivern/hippo/issues 329 | 330 | 331 | ## Security Issues 332 | 333 | If you discover a security vulnerability within Hippo, please send an email to [hello@clivern.com](mailto:hello@clivern.com) 334 | 335 | 336 | ## Contributing 337 | 338 | We are an open source, community-driven project so please feel free to join us. see the [contributing guidelines](CONTRIBUTING.md) for more details. 339 | 340 | 341 | ## License 342 | 343 | © 2019, Clivern. Released under [MIT License](https://opensource.org/licenses/mit-license.php). 344 | 345 | **Hippo** is authored and maintained by [@Clivern](http://github.com/clivern). 346 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 7 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 8 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 9 | github.com/go-redis/redis v6.15.8+incompatible h1:BKZuG6mCnRj5AOaWJXoCgf6rqTYnYJLe4en2hxT7r9o= 10 | github.com/go-redis/redis v6.15.8+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= 11 | github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= 12 | github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= 13 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 14 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 15 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 16 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 17 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 18 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 19 | github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= 20 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 21 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 22 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 23 | github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= 24 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 25 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 26 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 27 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 28 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 29 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 30 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 31 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 32 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 33 | github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4= 34 | github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= 35 | github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= 36 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 37 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 38 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 39 | github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA= 40 | github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= 41 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 42 | github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= 43 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 44 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 45 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 46 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 47 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 48 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 49 | github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= 50 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 51 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 52 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 53 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 54 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 55 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 56 | go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk= 57 | go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 58 | go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= 59 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 60 | go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A= 61 | go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 62 | go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= 63 | go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 64 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= 65 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 66 | go.uber.org/zap v1.15.0 h1:ZZCA22JRF2gQE5FoNmhmrf7jeJJ2uhqDUNRYKm8dvmM= 67 | go.uber.org/zap v1.15.0/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc= 68 | go.uber.org/zap v1.16.0 h1:uFRZXykJGK9lLY4HtgSw44DnIcAM+kRBP7x5m+NpAOM= 69 | go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= 70 | go.uber.org/zap v1.17.0 h1:MTjgFu6ZLKvY6Pvaqk97GlxNBuMpV4Hy/3P6tRGlI2U= 71 | go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= 72 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 73 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 74 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= 75 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 76 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 77 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 78 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 79 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 80 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 81 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7 h1:AeiKBIuRw3UomYXSbLy0Mc2dDLfdtbT/IVn4keq83P0= 82 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 83 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 84 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 85 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 86 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 87 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 88 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 89 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 90 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 91 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 92 | golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 h1:DYfZAGf2WMFjMxbgTjaC+2HC7NkNAQs+6Q8b9WEB/F4= 93 | golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 94 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 95 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 96 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 97 | golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e h1:EHBhcS0mlXEAVwNyO2dLfjToGsyY4j24pTs2ScHnX7s= 98 | golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 99 | golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba h1:O8mE0/t419eoIwhTFpKVkHiTs/Igowgfkj25AcZrtiE= 100 | golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 101 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 102 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 103 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 104 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 105 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs= 106 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 107 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 108 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 109 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 110 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 111 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 112 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 113 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 114 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 115 | google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= 116 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 117 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 118 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 119 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 120 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 121 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 122 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 123 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 124 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 125 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 126 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 127 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 128 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= 129 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 130 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 131 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 132 | honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= 133 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 134 | --------------------------------------------------------------------------------