├── .github ├── dependabot.yml └── workflows │ ├── hf_pr-dependency-review.yml │ └── testing.yml ├── .gitignore ├── .golangci.yml ├── .whitesource ├── LICENSE.txt ├── README.md ├── Taskfile.yaml ├── _examples └── server.go ├── checks ├── cassandra │ ├── check.go │ └── check_test.go ├── grpc │ ├── check.go │ └── check_test.go ├── http │ ├── check.go │ └── check_test.go ├── influxdb │ ├── check.go │ └── check_test.go ├── memcached │ ├── check.go │ └── check_test.go ├── mongo │ ├── check.go │ └── check_test.go ├── mysql │ ├── check.go │ └── check_test.go ├── nats │ ├── check.go │ └── check_test.go ├── pgx4 │ ├── check.go │ └── check_test.go ├── pgx5 │ ├── check.go │ └── check_test.go ├── postgres │ ├── check.go │ └── check_test.go ├── rabbitmq │ ├── aliveness_check_test.go │ ├── check.go │ └── check_test.go └── redis │ ├── check.go │ └── check_test.go ├── docker-compose.yml ├── docs └── index.md ├── go.mod ├── go.sum ├── health.go ├── health_test.go ├── mkdocs.yml ├── options.go └── options_test.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | rebase-strategy: "disabled" 6 | schedule: 7 | interval: "daily" -------------------------------------------------------------------------------- /.github/workflows/hf_pr-dependency-review.yml: -------------------------------------------------------------------------------- 1 | # This workflow is centrally managed in 2 | # https://github.com/hellofresh/github-automation/blob/master/modules/repository/shared-workflows/pr-dependency-review.yml 3 | 4 | # This workflow is for dependency review. It is used to check vulnerability in dependencies before merging the PR. 5 | # It is managed by squad-vulnerability-management. 6 | 7 | --- 8 | name: Dependency Review PR 9 | 10 | on: [pull_request] 11 | 12 | jobs: 13 | pull_request_review: 14 | permissions: 15 | contents: read 16 | pull-requests: write 17 | name: Dependency Review 18 | uses: hellofresh/ghas-rules/.github/workflows/dependency-review-reusable.yml@master 19 | -------------------------------------------------------------------------------- /.github/workflows/testing.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Testing 3 | 4 | on: 5 | push: 6 | branches: [ master ] 7 | pull_request: 8 | branches: [ master ] 9 | release: 10 | types: 11 | - created 12 | 13 | jobs: 14 | lint: 15 | name: Lint 16 | runs-on: ubuntu-latest 17 | timeout-minutes: 10 18 | steps: 19 | - uses: actions/checkout@v3 20 | - uses: actions/setup-go@v3 21 | with: 22 | go-version-file: ./go.mod 23 | - uses: golangci/golangci-lint-action@v3 24 | 25 | test: 26 | name: Test 27 | runs-on: ubuntu-latest 28 | timeout-minutes: 10 29 | strategy: 30 | matrix: 31 | go-version: [ '1.23', '1.24' ] 32 | 33 | steps: 34 | - uses: actions/checkout@v3 35 | - uses: actions/setup-go@v3 36 | with: 37 | go-version: ${{ matrix.go-version }} 38 | 39 | - name: Install Task 40 | uses: arduino/setup-task@v1 41 | with: 42 | repo-token: ${{ secrets.GITHUB_TOKEN }} 43 | 44 | - name: Install docker compose 45 | uses: docker/setup-compose-action@v1 46 | 47 | - run: task test 48 | 49 | tets-summary: 50 | name: Test 51 | needs: test 52 | runs-on: ubuntu-latest 53 | timeout-minutes: 5 54 | steps: 55 | - run: echo "All good!" 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | coverage.txt 3 | .idea/ 4 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # See https://golangci-lint.run/usage/configuration/#config-file for more information 3 | run: 4 | timeout: 5m 5 | linters: 6 | disable-all: true 7 | enable: 8 | - gofmt 9 | - revive 10 | - goimports 11 | fast: false 12 | linters-settings: 13 | gofmt: 14 | simplify: false 15 | issues: 16 | exclude-use-default: false 17 | exclude-rules: 18 | - linters: 19 | - revive 20 | text: "package-comments:" 21 | -------------------------------------------------------------------------------- /.whitesource: -------------------------------------------------------------------------------- 1 | { 2 | "scanSettings": { 3 | "configMode": "AUTO", 4 | "configExternalURL": "", 5 | "projectToken" : "" 6 | }, 7 | "checkRunSettings": { 8 | "vulnerableCheckRunConclusionLevel": "success" 9 | }, 10 | "issueSettings": { 11 | "minSeverityLevel": "NONE" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2020 HelloFresh SE 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # health-go 2 | 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/hellofresh/health-go)](https://goreportcard.com/report/github.com/hellofresh/health-go) 4 | [![Go Doc](https://godoc.org/github.com/hellofresh/health-go?status.svg)](https://godoc.org/github.com/hellofresh/health-go) 5 | 6 | * Exposes an HTTP handler that retrieves health status of the application 7 | * Implements some generic checkers for the following services: 8 | * RabbitMQ 9 | * PostgreSQL 10 | * Redis 11 | * HTTP 12 | * MongoDB 13 | * MySQL 14 | * gRPC 15 | * Memcached 16 | * InfluxDB 17 | * Nats 18 | 19 | ## Usage 20 | 21 | The library exports `Handler` and `HandlerFunc` functions which are fully compatible with `net/http`. 22 | 23 | Additionally, library exports `Measure` function that returns summary status for all the registered health checks, 24 | so it can be used in non-HTTP environments. 25 | 26 | ### Handler 27 | 28 | ```go 29 | package main 30 | 31 | import ( 32 | "context" 33 | "net/http" 34 | "time" 35 | 36 | "github.com/hellofresh/health-go/v5" 37 | healthMysql "github.com/hellofresh/health-go/v5/checks/mysql" 38 | ) 39 | 40 | func main() { 41 | // add some checks on instance creation 42 | h, _ := health.New(health.WithComponent(health.Component{ 43 | Name: "myservice", 44 | Version: "v1.0", 45 | }), health.WithChecks(health.Config{ 46 | Name: "rabbitmq", 47 | Timeout: time.Second * 5, 48 | SkipOnErr: true, 49 | Check: func(ctx context.Context) error { 50 | // rabbitmq health check implementation goes here 51 | return nil 52 | }}, health.Config{ 53 | Name: "mongodb", 54 | Check: func(ctx context.Context) error { 55 | // mongo_db health check implementation goes here 56 | return nil 57 | }, 58 | }, 59 | )) 60 | 61 | // and then add some more if needed 62 | h.Register(health.Config{ 63 | Name: "mysql", 64 | Timeout: time.Second * 2, 65 | SkipOnErr: false, 66 | Check: healthMysql.New(healthMysql.Config{ 67 | DSN: "test:test@tcp(0.0.0.0:31726)/test?charset=utf8", 68 | }), 69 | }) 70 | 71 | http.Handle("/status", h.Handler()) 72 | http.ListenAndServe(":3000", nil) 73 | } 74 | ``` 75 | 76 | ### HandlerFunc 77 | ```go 78 | package main 79 | 80 | import ( 81 | "context" 82 | "net/http" 83 | "time" 84 | 85 | "github.com/go-chi/chi" 86 | "github.com/hellofresh/health-go/v5" 87 | healthMysql "github.com/hellofresh/health-go/v5/checks/mysql" 88 | ) 89 | 90 | func main() { 91 | // add some checks on instance creation 92 | h, _ := health.New(health.WithComponent(health.Component{ 93 | Name: "myservice", 94 | Version: "v1.0", 95 | }), health.WithChecks(health.Config{ 96 | Name: "rabbitmq", 97 | Timeout: time.Second * 5, 98 | SkipOnErr: true, 99 | Check: func(ctx context.Context) error { 100 | // rabbitmq health check implementation goes here 101 | return nil 102 | }}, health.Config{ 103 | Name: "mongodb", 104 | Check: func(ctx context.Context) error { 105 | // mongo_db health check implementation goes here 106 | return nil 107 | }, 108 | }, 109 | )) 110 | 111 | // and then add some more if needed 112 | h.Register(health.Config{ 113 | Name: "mysql", 114 | Timeout: time.Second * 2, 115 | SkipOnErr: false, 116 | Check: healthMysql.New(healthMysql.Config{ 117 | DSN: "test:test@tcp(0.0.0.0:31726)/test?charset=utf8", 118 | }), 119 | }) 120 | 121 | r := chi.NewRouter() 122 | r.Get("/status", h.HandlerFunc) 123 | http.ListenAndServe(":3000", nil) 124 | } 125 | ``` 126 | 127 | For more examples please check [here](https://github.com/hellofresh/health-go/blob/master/_examples/server.go) 128 | ## API Documentation 129 | 130 | ### `GET /status` 131 | 132 | Get the health of the application. 133 | - Method: `GET` 134 | - Endpoint: `/status` 135 | - Request: 136 | ``` 137 | curl localhost:3000/status 138 | ``` 139 | - Response: 140 | 141 | HTTP/1.1 200 OK 142 | ```json 143 | { 144 | "status": "OK", 145 | "timestamp": "2017-01-01T00:00:00.413567856+033:00", 146 | "system": { 147 | "version": "go1.8", 148 | "goroutines_count": 4, 149 | "total_alloc_bytes": 21321, 150 | "heap_objects_count": 21323, 151 | "alloc_bytes": 234523 152 | }, 153 | "component": { 154 | "name": "myservice", 155 | "version": "v1.0" 156 | } 157 | } 158 | ``` 159 | 160 | HTTP/1.1 200 OK 161 | ```json 162 | { 163 | "status": "Partially Available", 164 | "timestamp": "2017-01-01T00:00:00.413567856+033:00", 165 | "failures": { 166 | "rabbitmq": "Failed during rabbitmq health check" 167 | }, 168 | "system": { 169 | "version": "go1.8", 170 | "goroutines_count": 4, 171 | "total_alloc_bytes": 21321, 172 | "heap_objects_count": 21323, 173 | "alloc_bytes": 234523 174 | }, 175 | "component": { 176 | "name": "myservice", 177 | "version": "v1.0" 178 | } 179 | } 180 | ``` 181 | 182 | HTTP/1.1 503 Service Unavailable 183 | ```json 184 | { 185 | "status": "Unavailable", 186 | "timestamp": "2017-01-01T00:00:00.413567856+033:00", 187 | "failures": { 188 | "mongodb": "Failed during mongodb health check" 189 | }, 190 | "system": { 191 | "version": "go1.8", 192 | "goroutines_count": 4, 193 | "total_alloc_bytes": 21321, 194 | "heap_objects_count": 21323, 195 | "alloc_bytes": 234523 196 | }, 197 | "component": { 198 | "name": "myservice", 199 | "version": "v1.0" 200 | } 201 | } 202 | ``` 203 | 204 | ## Contributing 205 | - Fork it 206 | - Create your feature branch (`git checkout -b my-new-feature`) 207 | - Commit your changes (`git commit -am 'Add some feature'`) 208 | - Push to the branch (`git push origin my-new-feature`) 209 | - Create new Pull Request 210 | 211 | --- 212 | > GitHub [@hellofresh](https://github.com/hellofresh)  ·  213 | > Medium [@engineering.hellofresh](https://engineering.hellofresh.com) 214 | -------------------------------------------------------------------------------- /Taskfile.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: '3' 3 | 4 | env: 5 | CGO_ENABLED: 0 6 | 7 | tasks: 8 | test: 9 | summary: Run tests 10 | cmds: 11 | - task: test-deps-up 12 | - task: test-run 13 | - task: test-run 14 | vars: 15 | RACE: true 16 | - task: test-deps-down 17 | 18 | test-deps-up: 19 | summary: Starts test dependencies 20 | preconditions: 21 | - sh: docker compose version --short | grep '^2' 22 | msg: 'docker compose v2 is expected to be installed' 23 | cmds: 24 | - cmd: docker compose up --detach --wait 25 | 26 | test-deps-down: 27 | summary: Stops test dependencies 28 | cmds: 29 | - cmd: docker compose down -v 30 | 31 | test-run: 32 | summary: Runs tests, must have dependencies running in the docker compose 33 | cmds: 34 | - cmd: go test {{if .RACE}} -race {{end}} -timeout 2m -cover -coverprofile=coverage.txt -covermode=atomic ./... 35 | vars: 36 | PG_PQ_HOST: 37 | sh: docker compose port pg-pq 5432 38 | PG_PGX4_HOST: 39 | sh: docker compose port pg-pgx4 5432 40 | PG_PGX5_HOST: 41 | sh: docker compose port pg-pgx5 5432 42 | RABBIT_HOST_AMQP: 43 | sh: docker compose port rabbit 5672 44 | RABBIT_HOST_HTTP: 45 | sh: docker compose port rabbit 15672 46 | REDIS_HOST: 47 | sh: docker compose port redis 6379 48 | MONGO_HOST: 49 | sh: docker compose port mongo 27017 50 | MYSQL_HOST: 51 | sh: docker compose port mysql 3306 52 | MEMCACHED_HOST: 53 | sh: docker compose port memcached 11211 54 | INFLUX_HOST: 55 | sh: docker compose port influxdb 8086 56 | CASSANDRA_HOST: 57 | sh: docker compose port cassandra 9042 58 | NATS_HOST: 59 | sh: docker compose port nats 4222 60 | 61 | env: 62 | CGO_ENABLED: '{{if .RACE}}1{{else}}0{{end}}' 63 | HEALTH_GO_PG_PQ_DSN: 'postgres://test:test@{{.PG_PQ_HOST}}/test?sslmode=disable' 64 | HEALTH_GO_PG_PGX4_DSN: 'postgres://test:test@{{.PG_PGX4_HOST}}/test?sslmode=disable' 65 | HEALTH_GO_PG_PGX5_DSN: 'postgres://test:test@{{.PG_PGX5_HOST}}/test?sslmode=disable' 66 | HEALTH_GO_MQ_DSN: 'amqp://guest:guest@{{.RABBIT_HOST_AMQP}}/' 67 | HEALTH_GO_MQ_URL: 'http://guest:guest@{{.RABBIT_HOST_HTTP}}/' 68 | HEALTH_GO_RD_DSN: 'redis://{{.REDIS_HOST}}/' 69 | HEALTH_GO_MG_DSN: 'mongodb://{{.MONGO_HOST}}/' 70 | HEALTH_GO_MS_DSN: 'test:test@tcp({{.MYSQL_HOST}})/test?charset=utf8' 71 | HEALTH_GO_MD_DSN: 'memcached://localhost:{{.MEMCACHED_HOST}}/' 72 | HEALTH_GO_INFLUXDB_URL: 'http://{{.INFLUX_HOST}}' 73 | HEALTH_GO_CASSANDRA_HOST: '{{.CASSANDRA_HOST}}' 74 | HEALTH_GO_NATS_DSN: 'nats://{{.NATS_HOST}}' 75 | -------------------------------------------------------------------------------- /_examples/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/hellofresh/health-go/v5" 10 | healthHttp "github.com/hellofresh/health-go/v5/checks/http" 11 | healthMySql "github.com/hellofresh/health-go/v5/checks/mysql" 12 | healthPg "github.com/hellofresh/health-go/v5/checks/postgres" 13 | ) 14 | 15 | func main() { 16 | h, _ := health.New(health.WithSystemInfo()) 17 | // custom health check example (fail) 18 | h.Register(health.Config{ 19 | Name: "some-custom-check-fail", 20 | Timeout: time.Second * 5, 21 | SkipOnErr: true, 22 | Check: func(context.Context) error { return errors.New("failed during custom health check") }, 23 | }) 24 | 25 | // custom health check example (success) 26 | h.Register(health.Config{ 27 | Name: "some-custom-check-success", 28 | Check: func(context.Context) error { return nil }, 29 | }) 30 | 31 | // http health check example 32 | h.Register(health.Config{ 33 | Name: "http-check", 34 | Timeout: time.Second * 5, 35 | SkipOnErr: true, 36 | Check: healthHttp.New(healthHttp.Config{ 37 | URL: `http://example.com`, 38 | }), 39 | }) 40 | 41 | // postgres health check example 42 | h.Register(health.Config{ 43 | Name: "postgres-check", 44 | Timeout: time.Second * 5, 45 | SkipOnErr: true, 46 | Check: healthPg.New(healthPg.Config{ 47 | DSN: `postgres://test:test@0.0.0.0:32783/test?sslmode=disable`, 48 | }), 49 | }) 50 | 51 | // mysql health check example 52 | h.Register(health.Config{ 53 | Name: "mysql-check", 54 | Timeout: time.Second * 5, 55 | SkipOnErr: true, 56 | Check: healthMySql.New(healthMySql.Config{ 57 | DSN: `test:test@tcp(0.0.0.0:32778)/test?charset=utf8`, 58 | }), 59 | }) 60 | 61 | // rabbitmq aliveness test example. 62 | // Use it if your app has access to RabbitMQ management API. 63 | // This endpoint declares a test queue, then publishes and consumes a message. Intended for use by monitoring tools. If everything is working correctly, will return HTTP status 200. 64 | // As the default virtual host is called "/", this will need to be encoded as "%2f". 65 | h.Register(health.Config{ 66 | Name: "rabbit-aliveness-check", 67 | Timeout: time.Second * 5, 68 | SkipOnErr: true, 69 | Check: healthHttp.New(healthHttp.Config{ 70 | URL: `http://guest:guest@0.0.0.0:32780/api/aliveness-test/%2f`, 71 | }), 72 | }) 73 | 74 | http.Handle("/status", h.Handler()) 75 | http.ListenAndServe(":3000", nil) 76 | } 77 | -------------------------------------------------------------------------------- /checks/cassandra/check.go: -------------------------------------------------------------------------------- 1 | package cassandra 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | 8 | "github.com/gocql/gocql" 9 | ) 10 | 11 | // Config is the Cassandra checker configuration settings container. 12 | type Config struct { 13 | // Hosts is a list of Cassandra hosts. Optional if Session is supplied. 14 | Hosts []string 15 | // Keyspace is the Cassandra keyspace to which you want to connect. Optional if Session is supplied. 16 | Keyspace string 17 | // Session is a gocql session and can be used in place of Hosts and Keyspace. Recommended. 18 | // Optional if Hosts & Keyspace are supplied. 19 | Session *gocql.Session 20 | } 21 | 22 | // New creates new Cassandra health check that verifies that a connection exists and can be used to query the cluster. 23 | func New(config Config) func(ctx context.Context) error { 24 | return func(ctx context.Context) error { 25 | shutdown, session, err := initSession(config) 26 | if err != nil { 27 | return fmt.Errorf("cassandra health check failed on connect: %w", err) 28 | } 29 | 30 | defer shutdown() 31 | 32 | if err != nil { 33 | return fmt.Errorf("cassandra health check failed on connect: %w", err) 34 | } 35 | 36 | err = session.Query("SELECT * FROM system_schema.keyspaces;").WithContext(ctx).Exec() 37 | if err != nil { 38 | return fmt.Errorf("cassandra health check failed on describe: %w", err) 39 | } 40 | 41 | return nil 42 | } 43 | } 44 | 45 | func initSession(c Config) (func(), *gocql.Session, error) { 46 | if c.Session != nil { 47 | return func() {}, c.Session, nil 48 | } 49 | 50 | if len(c.Hosts) < 1 || len(c.Keyspace) < 1 { 51 | return nil, nil, errors.New("cassandra cluster config or keyspace name and hosts are required to initialize cassandra health check") 52 | } 53 | 54 | cluster := gocql.NewCluster(c.Hosts...) 55 | cluster.Keyspace = c.Keyspace 56 | session, err := cluster.CreateSession() 57 | if err != nil { 58 | return nil, nil, err 59 | } 60 | 61 | return session.Close, session, err 62 | } 63 | -------------------------------------------------------------------------------- /checks/cassandra/check_test.go: -------------------------------------------------------------------------------- 1 | package cassandra 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/gocql/gocql" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | const HOST = "HEALTH_GO_CASSANDRA_HOST" 15 | const KEYSPACE = "default" 16 | 17 | func TestNew(t *testing.T) { 18 | initDB(t) 19 | 20 | check := New(Config{ 21 | Hosts: getHosts(t), 22 | Keyspace: KEYSPACE, 23 | }) 24 | 25 | err := check(context.Background()) 26 | require.NoError(t, err) 27 | } 28 | 29 | func TestNew_withClusterConfig(t *testing.T) { 30 | initDB(t) 31 | cluster := gocql.NewCluster(getHosts(t)...) 32 | cluster.Keyspace = KEYSPACE 33 | session, err := cluster.CreateSession() 34 | require.NoError(t, err) 35 | 36 | check := New(Config{ 37 | Session: session, 38 | }) 39 | 40 | err = check(context.Background()) 41 | require.NoError(t, err) 42 | } 43 | 44 | func TestNewWithError(t *testing.T) { 45 | check := New(Config{}) 46 | 47 | err := check(context.Background()) 48 | require.Error(t, err) 49 | } 50 | 51 | func initDB(t *testing.T) { 52 | t.Helper() 53 | 54 | cluster := gocql.NewCluster(getHosts(t)[0]) 55 | 56 | session, err := cluster.CreateSession() 57 | require.NoError(t, err) 58 | 59 | defer session.Close() 60 | 61 | err = session.Query(fmt.Sprintf("CREATE KEYSPACE IF NOT EXISTS %s WITH REPLICATION = {'class': 'SimpleStrategy', 'replication_factor': 1};", KEYSPACE)).Exec() 62 | require.NoError(t, err) 63 | 64 | err = session.Query(fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s.test (id UUID PRIMARY KEY, name text);", KEYSPACE)).Exec() 65 | require.NoError(t, err) 66 | } 67 | 68 | func getHosts(t *testing.T) []string { 69 | t.Helper() 70 | 71 | host, ok := os.LookupEnv(HOST) 72 | require.True(t, ok, fmt.Sprintf("Host is: %s", host)) 73 | 74 | // "docker compose port " returns 0.0.0.0:XXXX locally, change it to local port 75 | host = strings.Replace(host, "0.0.0.0:", "127.0.0.1:", 1) 76 | 77 | return []string{host} 78 | } 79 | -------------------------------------------------------------------------------- /checks/grpc/check.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "google.golang.org/grpc" 9 | "google.golang.org/grpc/health/grpc_health_v1" 10 | ) 11 | 12 | const defaultCheckTimeout = 5 * time.Second 13 | 14 | // Config is the gRPC checker configuration settings container. 15 | type Config struct { 16 | // Target is the address of the gRPC server 17 | Target string 18 | // Service is the name of the gRPC service 19 | Service string 20 | // DialOptions configure how we set up the connection 21 | DialOptions []grpc.DialOption 22 | // CheckTimeout is the duration that health check will try to get gRPC service health status. 23 | // If not set - 5 seconds 24 | CheckTimeout time.Duration 25 | } 26 | 27 | // New creates new gRPC health check 28 | func New(config Config) func(ctx context.Context) error { 29 | if config.CheckTimeout == 0 { 30 | config.CheckTimeout = defaultCheckTimeout 31 | } 32 | 33 | return func(ctx context.Context) error { 34 | // Set up a connection to the gRPC server 35 | conn, err := grpc.Dial(config.Target, config.DialOptions...) 36 | if err != nil { 37 | return fmt.Errorf("gRPC health check failed on connect: %w", err) 38 | } 39 | defer conn.Close() 40 | 41 | healthClient := grpc_health_v1.NewHealthClient(conn) 42 | 43 | ctx, cancel := context.WithTimeout(ctx, config.CheckTimeout) 44 | defer cancel() 45 | 46 | res, err := healthClient.Check(ctx, &grpc_health_v1.HealthCheckRequest{ 47 | Service: config.Service, 48 | }) 49 | if err != nil { 50 | return fmt.Errorf("gRPC health check failed on check call: %w", err) 51 | } 52 | 53 | if res.GetStatus() != grpc_health_v1.HealthCheckResponse_SERVING { 54 | return fmt.Errorf("gRPC service reported as non-serving: %q", res.GetStatus().String()) 55 | } 56 | 57 | return nil 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /checks/grpc/check_test.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | "google.golang.org/grpc" 10 | "google.golang.org/grpc/health" 11 | "google.golang.org/grpc/health/grpc_health_v1" 12 | ) 13 | 14 | func TestNew(t *testing.T) { 15 | for name, tc := range map[string]struct { 16 | servingStatus grpc_health_v1.HealthCheckResponse_ServingStatus 17 | requireError bool 18 | }{ 19 | "serving": { 20 | servingStatus: grpc_health_v1.HealthCheckResponse_SERVING, 21 | requireError: false, 22 | }, 23 | "unknown": { 24 | servingStatus: grpc_health_v1.HealthCheckResponse_UNKNOWN, 25 | requireError: true, 26 | }, 27 | "not serving": { 28 | servingStatus: grpc_health_v1.HealthCheckResponse_NOT_SERVING, 29 | requireError: true, 30 | }, 31 | } { 32 | servingStatus := tc.servingStatus 33 | requireError := tc.requireError 34 | 35 | t.Run(name, func(t *testing.T) { 36 | t.Parallel() 37 | 38 | const service = "HealthTest" 39 | 40 | healthServer := health.NewServer() 41 | healthServer.SetServingStatus(service, servingStatus) 42 | 43 | lis, err := net.Listen("tcp", "localhost:0") 44 | require.NoError(t, err) 45 | 46 | server := grpc.NewServer() 47 | grpc_health_v1.RegisterHealthServer(server, healthServer) 48 | 49 | go func() { 50 | if err := server.Serve(lis); err != nil { 51 | t.Log("Failed to serve GRPC", err) 52 | } 53 | }() 54 | defer server.Stop() 55 | 56 | check := New(Config{ 57 | Target: lis.Addr().String(), 58 | Service: service, 59 | DialOptions: []grpc.DialOption{ 60 | grpc.WithInsecure(), 61 | }, 62 | }) 63 | 64 | err = check(context.Background()) 65 | 66 | if requireError { 67 | require.Error(t, err) 68 | } else { 69 | require.NoError(t, err) 70 | } 71 | }) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /checks/http/check.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "time" 9 | ) 10 | 11 | const defaultRequestTimeout = 5 * time.Second 12 | 13 | // Config is the HTTP checker configuration settings container. 14 | type Config struct { 15 | // URL is the remote service health check URL. 16 | URL string 17 | // RequestTimeout is the duration that health check will try to consume published test message. 18 | // If not set - 5 seconds 19 | RequestTimeout time.Duration 20 | } 21 | 22 | // New creates new HTTP service health check that verifies the following: 23 | // - connection establishing 24 | // - getting response status from defined URL 25 | // - verifying that status code is less than 500 26 | func New(config Config) func(ctx context.Context) error { 27 | if config.RequestTimeout == 0 { 28 | config.RequestTimeout = defaultRequestTimeout 29 | } 30 | 31 | return func(ctx context.Context) error { 32 | req, err := http.NewRequest(http.MethodGet, config.URL, nil) 33 | if err != nil { 34 | return fmt.Errorf("creating the request for the health check failed: %w", err) 35 | } 36 | 37 | ctx, cancel := context.WithTimeout(ctx, config.RequestTimeout) 38 | defer cancel() 39 | 40 | // Inform remote service to close the connection after the transaction is complete 41 | req.Header.Set("Connection", "close") 42 | req = req.WithContext(ctx) 43 | 44 | res, err := http.DefaultClient.Do(req) 45 | if err != nil { 46 | return fmt.Errorf("making the request for the health check failed: %w", err) 47 | } 48 | defer res.Body.Close() 49 | 50 | if res.StatusCode >= http.StatusInternalServerError { 51 | return errors.New("remote service is not available at the moment") 52 | } 53 | 54 | return nil 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /checks/http/check_test.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | "github.com/vitorsalgado/mocha/v2" 10 | "github.com/vitorsalgado/mocha/v2/expect" 11 | "github.com/vitorsalgado/mocha/v2/reply" 12 | ) 13 | 14 | func TestNew(t *testing.T) { 15 | m := mocha.New(t) 16 | m.Start() 17 | m.CloseOnCleanup(t) 18 | 19 | t.Run("service is available", func(t *testing.T) { 20 | svc200 := m.AddMocks(mocha.Get(expect.URLPath("/test-200")).Reply(reply.OK())) 21 | 22 | check := New(Config{ 23 | URL: m.URL() + "/test-200", 24 | }) 25 | 26 | err := check(context.Background()) 27 | require.NoError(t, err) 28 | assert.True(t, svc200.Called()) 29 | }) 30 | 31 | t.Run("service is not available", func(t *testing.T) { 32 | svc500 := m.AddMocks(mocha.Get(expect.URLPath("/test-500")).Reply(reply.InternalServerError())) 33 | 34 | check := New(Config{ 35 | URL: m.URL() + "/test-500", 36 | }) 37 | 38 | err := check(context.Background()) 39 | require.Error(t, err) 40 | assert.True(t, svc500.Called()) 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /checks/influxdb/check.go: -------------------------------------------------------------------------------- 1 | // Package influxdb implements a health check for InfluxDB instance. 2 | package influxdb 3 | 4 | import ( 5 | "context" 6 | "errors" 7 | "fmt" 8 | 9 | influxdb2 "github.com/influxdata/influxdb-client-go/v2" 10 | "github.com/influxdata/influxdb-client-go/v2/domain" 11 | ) 12 | 13 | // Config stores InfluxDB API host and possibly parameters. 14 | type Config struct { 15 | URL string 16 | } 17 | 18 | // New returns a check function. It uses InfluxDB health api 19 | // to get status of the instance. 20 | func New(config Config) func(ctx context.Context) error { 21 | return func(ctx context.Context) error { 22 | // since only health api will be used, we don't need to pass 23 | // any Authorization data (token in this case) 24 | client := influxdb2.NewClient(config.URL, "") 25 | defer client.Close() 26 | 27 | h, err := client.Health(ctx) 28 | 29 | if err != nil { 30 | return fmt.Errorf("InfluxDB health check failed: %w", err) 31 | } 32 | 33 | // any status different from "pass" is considered as failed 34 | if h.Status != domain.HealthCheckStatusPass { 35 | return errors.New("InfluxDB health check failed, didn't get PASS status") 36 | } 37 | 38 | return nil 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /checks/influxdb/check_test.go: -------------------------------------------------------------------------------- 1 | package influxdb 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | const InfluxDbURLEnv = "HEALTH_GO_INFLUXDB_URL" 12 | 13 | func TestNew(t *testing.T) { 14 | check := New(Config{ 15 | URL: getURL(t), 16 | }) 17 | 18 | err := check(context.Background()) 19 | require.NoError(t, err) 20 | } 21 | 22 | func TestNewWithError(t *testing.T) { 23 | check := New(Config{ 24 | URL: "", 25 | }) 26 | 27 | err := check(context.Background()) 28 | require.Error(t, err) 29 | } 30 | 31 | func getURL(t *testing.T) string { 32 | t.Helper() 33 | 34 | url, ok := os.LookupEnv(InfluxDbURLEnv) 35 | 36 | require.True(t, ok) 37 | 38 | return url 39 | } 40 | -------------------------------------------------------------------------------- /checks/memcached/check.go: -------------------------------------------------------------------------------- 1 | package memcached 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/bradfitz/gomemcache/memcache" 9 | ) 10 | 11 | // Config is the Memcached checker configuration settings container. 12 | type Config struct { 13 | // DSN is the Memcached instance connection DSN. Required. 14 | DSN string 15 | } 16 | 17 | // New creates new Memcached health check that verifies the following: 18 | // - connection establishing 19 | // - doing the PING command and verifying the response 20 | func New(config Config) func(ctx context.Context) error { 21 | // support all DSN formats (for backward compatibility) - with and w/out schema and path part: 22 | // - memcached://localhost:11211/ 23 | // - localhost:11211 24 | memcachedDSN := strings.TrimPrefix(config.DSN, "memcached://") 25 | memcachedDSN = strings.TrimSuffix(memcachedDSN, "/") 26 | 27 | return func(_ context.Context) error { 28 | mdb := memcache.New(memcachedDSN) 29 | 30 | err := mdb.Ping() 31 | 32 | if err != nil { 33 | return fmt.Errorf("memcached ping failed: %w", err) 34 | } 35 | 36 | return nil 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /checks/memcached/check_test.go: -------------------------------------------------------------------------------- 1 | package memcached 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | const rdDSNEnv = "HEALTH_GO_MD_DSN" 12 | 13 | func TestNew(t *testing.T) { 14 | check := New(Config{ 15 | DSN: getDSN(t), 16 | }) 17 | 18 | err := check(context.Background()) 19 | require.NoError(t, err) 20 | } 21 | 22 | func TestNewError(t *testing.T) { 23 | check := New(Config{ 24 | DSN: "", 25 | }) 26 | 27 | err := check(context.Background()) 28 | require.Error(t, err) 29 | } 30 | 31 | func getDSN(t *testing.T) string { 32 | t.Helper() 33 | 34 | redisDSN, ok := os.LookupEnv(rdDSNEnv) 35 | require.True(t, ok) 36 | 37 | return redisDSN 38 | } 39 | -------------------------------------------------------------------------------- /checks/mongo/check.go: -------------------------------------------------------------------------------- 1 | package mongo 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "go.mongodb.org/mongo-driver/mongo" 9 | "go.mongodb.org/mongo-driver/mongo/options" 10 | "go.mongodb.org/mongo-driver/mongo/readpref" 11 | ) 12 | 13 | const ( 14 | defaultTimeoutConnect = 5 * time.Second 15 | defaultTimeoutDisconnect = 5 * time.Second 16 | defaultTimeoutPing = 5 * time.Second 17 | ) 18 | 19 | // Config is the MongoDB checker configuration settings container. 20 | type Config struct { 21 | // DSN is the MongoDB instance connection DSN. Required. 22 | DSN string 23 | 24 | // TimeoutConnect defines timeout for establishing mongo connection, if not set - default value is used 25 | TimeoutConnect time.Duration 26 | // TimeoutDisconnect defines timeout for closing connection, if not set - default value is used 27 | TimeoutDisconnect time.Duration 28 | // TimeoutDisconnect defines timeout for making ping request, if not set - default value is used 29 | TimeoutPing time.Duration 30 | } 31 | 32 | // New creates new MongoDB health check that verifies the following: 33 | // - connection establishing 34 | // - doing the ping command 35 | func New(config Config) func(ctx context.Context) error { 36 | if config.TimeoutConnect == 0 { 37 | config.TimeoutConnect = defaultTimeoutConnect 38 | } 39 | 40 | if config.TimeoutDisconnect == 0 { 41 | config.TimeoutDisconnect = defaultTimeoutDisconnect 42 | } 43 | 44 | if config.TimeoutPing == 0 { 45 | config.TimeoutPing = defaultTimeoutPing 46 | } 47 | 48 | return func(ctx context.Context) (checkErr error) { 49 | client, err := mongo.NewClient(options.Client().ApplyURI(config.DSN)) 50 | if err != nil { 51 | checkErr = fmt.Errorf("mongoDB health check failed on client creation: %w", err) 52 | return 53 | } 54 | 55 | ctxConn, cancelConn := context.WithTimeout(ctx, config.TimeoutConnect) 56 | defer cancelConn() 57 | 58 | err = client.Connect(ctxConn) 59 | if err != nil { 60 | checkErr = fmt.Errorf("mongoDB health check failed on connect: %w", err) 61 | return 62 | } 63 | 64 | defer func() { 65 | ctxDisc, cancelDisc := context.WithTimeout(ctx, config.TimeoutDisconnect) 66 | defer cancelDisc() 67 | 68 | // override checkErr only if there were no other errors 69 | if err := client.Disconnect(ctxDisc); err != nil && checkErr == nil { 70 | checkErr = fmt.Errorf("mongoDB health check failed on closing connection: %w", err) 71 | } 72 | }() 73 | 74 | ctxPing, cancelPing := context.WithTimeout(ctx, config.TimeoutPing) 75 | defer cancelPing() 76 | 77 | err = client.Ping(ctxPing, readpref.Primary()) 78 | if err != nil { 79 | checkErr = fmt.Errorf("mongoDB health check failed on ping: %w", err) 80 | return 81 | } 82 | 83 | return 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /checks/mongo/check_test.go: -------------------------------------------------------------------------------- 1 | package mongo 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | const mgDSNEnv = "HEALTH_GO_MG_DSN" 13 | 14 | func TestNew(t *testing.T) { 15 | check := New(Config{ 16 | DSN: getDSN(t), 17 | }) 18 | 19 | err := check(context.Background()) 20 | require.NoError(t, err) 21 | } 22 | 23 | func getDSN(t *testing.T) string { 24 | t.Helper() 25 | 26 | mongoDSN, ok := os.LookupEnv(mgDSNEnv) 27 | require.True(t, ok) 28 | 29 | // "docker compose port " returns 0.0.0.0:XXXX locally, change it to local port 30 | mongoDSN = strings.Replace(mongoDSN, "0.0.0.0:", "127.0.0.1:", 1) 31 | 32 | return mongoDSN 33 | } 34 | -------------------------------------------------------------------------------- /checks/mysql/check.go: -------------------------------------------------------------------------------- 1 | package mysql 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "fmt" 7 | 8 | _ "github.com/go-sql-driver/mysql" // import mysql driver 9 | ) 10 | 11 | // Config is the MySQL checker configuration settings container. 12 | type Config struct { 13 | // DSN is the MySQL instance connection DSN. Required. 14 | DSN string 15 | } 16 | 17 | // New creates new MySQL health check that verifies the following: 18 | // - connection establishing 19 | // - doing the ping command 20 | // - selecting mysql version 21 | func New(config Config) func(ctx context.Context) error { 22 | return func(ctx context.Context) (checkErr error) { 23 | db, err := sql.Open("mysql", config.DSN) 24 | if err != nil { 25 | checkErr = fmt.Errorf("MySQL health check failed on connect: %w", err) 26 | return 27 | } 28 | 29 | defer func() { 30 | // override checkErr only if there were no other errors 31 | if err = db.Close(); err != nil && checkErr == nil { 32 | checkErr = fmt.Errorf("MySQL health check failed on connection closing: %w", err) 33 | } 34 | }() 35 | 36 | err = db.PingContext(ctx) 37 | if err != nil { 38 | checkErr = fmt.Errorf("MySQL health check failed on ping: %w", err) 39 | return 40 | } 41 | 42 | rows, err := db.QueryContext(ctx, `SELECT VERSION()`) 43 | if err != nil { 44 | checkErr = fmt.Errorf("MySQL health check failed on select: %w", err) 45 | return 46 | } 47 | defer func() { 48 | // override checkErr only if there were no other errors 49 | if err = rows.Close(); err != nil && checkErr == nil { 50 | checkErr = fmt.Errorf("MySQL health check failed on rows closing: %w", err) 51 | } 52 | }() 53 | 54 | return 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /checks/mysql/check_test.go: -------------------------------------------------------------------------------- 1 | package mysql 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "os" 7 | "strings" 8 | "sync" 9 | "testing" 10 | "time" 11 | 12 | _ "github.com/go-sql-driver/mysql" 13 | "github.com/stretchr/testify/assert" 14 | "github.com/stretchr/testify/require" 15 | ) 16 | 17 | const mysqlDSNEnv = "HEALTH_GO_MS_DSN" 18 | 19 | func TestNew(t *testing.T) { 20 | initDB(t) 21 | 22 | check := New(Config{ 23 | DSN: getDSN(t), 24 | }) 25 | 26 | err := check(context.Background()) 27 | require.NoError(t, err) 28 | } 29 | 30 | func TestEnsureConnectionIsClosed(t *testing.T) { 31 | initDB(t) 32 | 33 | mysqlDSN := getDSN(t) 34 | 35 | db, err := sql.Open("mysql", mysqlDSN) 36 | require.NoError(t, err) 37 | 38 | defer func() { 39 | err := db.Close() 40 | assert.NoError(t, err) 41 | }() 42 | 43 | var ( 44 | varName string 45 | initialConnections int 46 | ) 47 | row := db.QueryRow(`SHOW STATUS WHERE variable_name = 'Threads_connected'`) 48 | err = row.Scan(&varName, &initialConnections) 49 | require.NoError(t, err) 50 | 51 | check := New(Config{ 52 | DSN: mysqlDSN, 53 | }) 54 | 55 | ctx := context.Background() 56 | for i := 0; i < 10; i++ { 57 | err := check(ctx) 58 | assert.NoError(t, err) 59 | time.Sleep(100 * time.Millisecond) 60 | } 61 | 62 | var currentConnections int 63 | row = db.QueryRow(`SHOW STATUS WHERE variable_name = 'Threads_connected'`) 64 | err = row.Scan(&varName, ¤tConnections) 65 | require.NoError(t, err) 66 | 67 | assert.Equal(t, initialConnections, currentConnections) 68 | } 69 | 70 | func getDSN(t *testing.T) string { 71 | t.Helper() 72 | 73 | mysqlDSN, ok := os.LookupEnv(mysqlDSNEnv) 74 | require.True(t, ok) 75 | 76 | // "docker compose port " returns 0.0.0.0:XXXX locally, change it to local port 77 | mysqlDSN = strings.Replace(mysqlDSN, "0.0.0.0:", "127.0.0.1:", 1) 78 | 79 | return mysqlDSN 80 | } 81 | 82 | var dbInit sync.Once 83 | 84 | func initDB(t *testing.T) { 85 | t.Helper() 86 | 87 | dbInit.Do(func() { 88 | db, err := sql.Open("mysql", getDSN(t)) 89 | require.NoError(t, err) 90 | 91 | defer func() { 92 | err := db.Close() 93 | assert.NoError(t, err) 94 | }() 95 | 96 | _, err = db.Exec(` 97 | CREATE TABLE IF NOT EXISTS test ( 98 | id INT NOT NULL AUTO_INCREMENT PRIMARY KEY , 99 | secret VARCHAR(256) NOT NULL, 100 | extra VARCHAR(256) NOT NULL, 101 | redirect_uri VARCHAR(256) NOT NULL 102 | ); 103 | `) 104 | require.NoError(t, err) 105 | }) 106 | } 107 | -------------------------------------------------------------------------------- /checks/nats/check.go: -------------------------------------------------------------------------------- 1 | package nats 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/nats-io/nats.go" 8 | ) 9 | 10 | // Config is the NATS checker configuration settings container. 11 | type Config struct { 12 | // DSN is the NATS instance connection DSN. Required. 13 | DSN string 14 | } 15 | 16 | // New creates new NATS health check that verifies the status of the connection. 17 | func New(config Config) func(ctx context.Context) error { 18 | return func(context.Context) error { 19 | nc, err := nats.Connect(config.DSN) 20 | if err != nil { 21 | return fmt.Errorf("nats health check failed on client creation: %w", err) 22 | } 23 | defer nc.Close() 24 | 25 | status := nc.Status() 26 | if status != nats.CONNECTED { 27 | return fmt.Errorf("nats health check failed as connection status is %s", status) 28 | } 29 | 30 | return nil 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /checks/nats/check_test.go: -------------------------------------------------------------------------------- 1 | package nats 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | const natsDSNEnv = "HEALTH_GO_NATS_DSN" 12 | 13 | func TestNew(t *testing.T) { 14 | check := New(Config{ 15 | DSN: getDSN(t), 16 | }) 17 | 18 | err := check(context.Background()) 19 | require.NoError(t, err) 20 | } 21 | 22 | func getDSN(t *testing.T) string { 23 | t.Helper() 24 | 25 | dsn, ok := os.LookupEnv(natsDSNEnv) 26 | require.True(t, ok) 27 | 28 | return dsn 29 | } 30 | -------------------------------------------------------------------------------- /checks/pgx4/check.go: -------------------------------------------------------------------------------- 1 | package pgx4 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/jackc/pgx/v4" 8 | ) 9 | 10 | // Config is the PostgreSQL checker configuration settings container. 11 | type Config struct { 12 | // DSN is the PostgreSQL instance connection DSN. Required. 13 | DSN string 14 | } 15 | 16 | // New creates new PostgreSQL health check that verifies the following: 17 | // - connection establishing 18 | // - doing the ping command 19 | // - selecting postgres version 20 | func New(config Config) func(ctx context.Context) error { 21 | return func(ctx context.Context) (checkErr error) { 22 | conn, err := pgx.Connect(ctx, config.DSN) 23 | if err != nil { 24 | checkErr = fmt.Errorf("PostgreSQL health check failed on connect: %w", err) 25 | return 26 | } 27 | 28 | defer func() { 29 | // override checkErr only if there were no other errors 30 | if err := conn.Close(ctx); err != nil && checkErr == nil { 31 | checkErr = fmt.Errorf("PostgreSQL health check failed on connection closing: %w", err) 32 | } 33 | }() 34 | 35 | err = conn.Ping(ctx) 36 | if err != nil { 37 | checkErr = fmt.Errorf("PostgreSQL health check failed on ping: %w", err) 38 | return 39 | } 40 | 41 | rows, err := conn.Query(ctx, `SELECT VERSION()`) 42 | if err != nil { 43 | checkErr = fmt.Errorf("PostgreSQL health check failed on select: %w", err) 44 | return 45 | } 46 | defer func() { 47 | rows.Close() 48 | }() 49 | 50 | return 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /checks/pgx4/check_test.go: -------------------------------------------------------------------------------- 1 | package pgx4 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "sync" 7 | "testing" 8 | "time" 9 | 10 | "github.com/jackc/pgx/v4" 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | const pgDSNEnv = "HEALTH_GO_PG_PGX4_DSN" 16 | 17 | func TestNew(t *testing.T) { 18 | initDB(t) 19 | 20 | check := New(Config{ 21 | DSN: getDSN(t), 22 | }) 23 | 24 | err := check(context.Background()) 25 | require.NoError(t, err) 26 | } 27 | 28 | func TestEnsureConnectionIsClosed(t *testing.T) { 29 | initDB(t) 30 | 31 | pgDSN := getDSN(t) 32 | ctx := context.Background() 33 | 34 | conn, err := pgx.Connect(ctx, getDSN(t)) 35 | require.NoError(t, err) 36 | 37 | defer func() { 38 | err := conn.Close(ctx) 39 | assert.NoError(t, err) 40 | }() 41 | 42 | var initialConnections int 43 | row := conn.QueryRow(ctx, `SELECT sum(numbackends) FROM pg_stat_database`) 44 | err = row.Scan(&initialConnections) 45 | require.NoError(t, err) 46 | 47 | check := New(Config{ 48 | DSN: pgDSN, 49 | }) 50 | 51 | for i := 0; i < 10; i++ { 52 | err := check(ctx) 53 | assert.NoError(t, err) 54 | time.Sleep(100 * time.Millisecond) 55 | } 56 | 57 | var currentConnections int 58 | row = conn.QueryRow(ctx, `SELECT sum(numbackends) FROM pg_stat_database`) 59 | err = row.Scan(¤tConnections) 60 | require.NoError(t, err) 61 | 62 | assert.Equal(t, initialConnections, currentConnections) 63 | } 64 | 65 | func getDSN(t *testing.T) string { 66 | t.Helper() 67 | 68 | pgDSN, ok := os.LookupEnv(pgDSNEnv) 69 | require.True(t, ok) 70 | 71 | return pgDSN 72 | } 73 | 74 | var dbInit sync.Once 75 | 76 | func initDB(t *testing.T) { 77 | t.Helper() 78 | 79 | dbInit.Do(func() { 80 | ctx := context.Background() 81 | 82 | conn, err := pgx.Connect(ctx, getDSN(t)) 83 | require.NoError(t, err) 84 | 85 | defer func() { 86 | err := conn.Close(ctx) 87 | assert.NoError(t, err) 88 | }() 89 | 90 | _, err = conn.Exec(ctx, ` 91 | CREATE TABLE IF NOT EXISTS test_pgx4 ( 92 | id TEXT NOT NULL PRIMARY KEY, 93 | secret TEXT NOT NULL, 94 | extra TEXT NOT NULL, 95 | redirect_uri TEXT NOT NULL 96 | ); 97 | `) 98 | require.NoError(t, err) 99 | }) 100 | } 101 | -------------------------------------------------------------------------------- /checks/pgx5/check.go: -------------------------------------------------------------------------------- 1 | package pgx5 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/jackc/pgx/v5" 8 | ) 9 | 10 | // Config is the PostgreSQL checker configuration settings container. 11 | type Config struct { 12 | // DSN is the PostgreSQL instance connection DSN. Required. 13 | DSN string 14 | } 15 | 16 | // New creates new PostgreSQL health check that verifies the following: 17 | // - connection establishing 18 | // - doing the ping command 19 | // - selecting postgres version 20 | func New(config Config) func(ctx context.Context) error { 21 | return func(ctx context.Context) (checkErr error) { 22 | conn, err := pgx.Connect(ctx, config.DSN) 23 | if err != nil { 24 | checkErr = fmt.Errorf("PostgreSQL health check failed on connect: %w", err) 25 | return 26 | } 27 | 28 | defer func() { 29 | // override checkErr only if there were no other errors 30 | if err := conn.Close(ctx); err != nil && checkErr == nil { 31 | checkErr = fmt.Errorf("PostgreSQL health check failed on connection closing: %w", err) 32 | } 33 | }() 34 | 35 | err = conn.Ping(ctx) 36 | if err != nil { 37 | checkErr = fmt.Errorf("PostgreSQL health check failed on ping: %w", err) 38 | return 39 | } 40 | 41 | rows, err := conn.Query(ctx, `SELECT VERSION()`) 42 | if err != nil { 43 | checkErr = fmt.Errorf("PostgreSQL health check failed on select: %w", err) 44 | return 45 | } 46 | defer func() { 47 | rows.Close() 48 | }() 49 | 50 | return 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /checks/pgx5/check_test.go: -------------------------------------------------------------------------------- 1 | package pgx5 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "sync" 7 | "testing" 8 | "time" 9 | 10 | "github.com/jackc/pgx/v5" 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | const pgDSNEnv = "HEALTH_GO_PG_PGX5_DSN" 16 | 17 | func TestNew(t *testing.T) { 18 | initDB(t) 19 | 20 | check := New(Config{ 21 | DSN: getDSN(t), 22 | }) 23 | 24 | err := check(context.Background()) 25 | require.NoError(t, err) 26 | } 27 | 28 | func TestEnsureConnectionIsClosed(t *testing.T) { 29 | initDB(t) 30 | 31 | pgDSN := getDSN(t) 32 | ctx := context.Background() 33 | 34 | conn, err := pgx.Connect(ctx, getDSN(t)) 35 | require.NoError(t, err) 36 | 37 | defer func() { 38 | err := conn.Close(ctx) 39 | assert.NoError(t, err) 40 | }() 41 | 42 | var initialConnections int 43 | row := conn.QueryRow(ctx, `SELECT sum(numbackends) FROM pg_stat_database`) 44 | err = row.Scan(&initialConnections) 45 | require.NoError(t, err) 46 | 47 | check := New(Config{ 48 | DSN: pgDSN, 49 | }) 50 | 51 | for i := 0; i < 10; i++ { 52 | err := check(ctx) 53 | assert.NoError(t, err) 54 | time.Sleep(100 * time.Millisecond) 55 | } 56 | 57 | var currentConnections int 58 | row = conn.QueryRow(ctx, `SELECT sum(numbackends) FROM pg_stat_database`) 59 | err = row.Scan(¤tConnections) 60 | require.NoError(t, err) 61 | 62 | assert.Equal(t, initialConnections, currentConnections) 63 | } 64 | 65 | func getDSN(t *testing.T) string { 66 | t.Helper() 67 | 68 | pgDSN, ok := os.LookupEnv(pgDSNEnv) 69 | require.True(t, ok) 70 | 71 | return pgDSN 72 | } 73 | 74 | var dbInit sync.Once 75 | 76 | func initDB(t *testing.T) { 77 | t.Helper() 78 | 79 | dbInit.Do(func() { 80 | ctx := context.Background() 81 | 82 | conn, err := pgx.Connect(ctx, getDSN(t)) 83 | require.NoError(t, err) 84 | 85 | defer func() { 86 | err := conn.Close(ctx) 87 | assert.NoError(t, err) 88 | }() 89 | 90 | _, err = conn.Exec(ctx, ` 91 | CREATE TABLE IF NOT EXISTS test_pgx4 ( 92 | id TEXT NOT NULL PRIMARY KEY, 93 | secret TEXT NOT NULL, 94 | extra TEXT NOT NULL, 95 | redirect_uri TEXT NOT NULL 96 | ); 97 | `) 98 | require.NoError(t, err) 99 | }) 100 | } 101 | -------------------------------------------------------------------------------- /checks/postgres/check.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "fmt" 7 | 8 | _ "github.com/lib/pq" // import pg driver 9 | ) 10 | 11 | // Config is the PostgreSQL checker configuration settings container. 12 | type Config struct { 13 | // DSN is the PostgreSQL instance connection DSN. Required. 14 | DSN string 15 | } 16 | 17 | // New creates new PostgreSQL health check that verifies the following: 18 | // - connection establishing 19 | // - doing the ping command 20 | // - selecting postgres version 21 | func New(config Config) func(ctx context.Context) error { 22 | return func(ctx context.Context) (checkErr error) { 23 | db, err := sql.Open("postgres", config.DSN) 24 | if err != nil { 25 | checkErr = fmt.Errorf("PostgreSQL health check failed on connect: %w", err) 26 | return 27 | } 28 | 29 | defer func() { 30 | // override checkErr only if there were no other errors 31 | if err := db.Close(); err != nil && checkErr == nil { 32 | checkErr = fmt.Errorf("PostgreSQL health check failed on connection closing: %w", err) 33 | } 34 | }() 35 | 36 | err = db.PingContext(ctx) 37 | if err != nil { 38 | checkErr = fmt.Errorf("PostgreSQL health check failed on ping: %w", err) 39 | return 40 | } 41 | 42 | rows, err := db.QueryContext(ctx, `SELECT VERSION()`) 43 | if err != nil { 44 | checkErr = fmt.Errorf("PostgreSQL health check failed on select: %w", err) 45 | return 46 | } 47 | defer func() { 48 | // override checkErr only if there were no other errors 49 | if err = rows.Close(); err != nil && checkErr == nil { 50 | checkErr = fmt.Errorf("PostgreSQL health check failed on rows closing: %w", err) 51 | } 52 | }() 53 | 54 | return 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /checks/postgres/check_test.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "os" 7 | "strings" 8 | "sync" 9 | "testing" 10 | "time" 11 | 12 | _ "github.com/lib/pq" 13 | "github.com/stretchr/testify/assert" 14 | "github.com/stretchr/testify/require" 15 | ) 16 | 17 | const pgDSNEnv = "HEALTH_GO_PG_PQ_DSN" 18 | 19 | func TestNew(t *testing.T) { 20 | initDB(t) 21 | 22 | check := New(Config{ 23 | DSN: getDSN(t), 24 | }) 25 | 26 | err := check(context.Background()) 27 | require.NoError(t, err) 28 | } 29 | 30 | func TestEnsureConnectionIsClosed(t *testing.T) { 31 | initDB(t) 32 | 33 | pgDSN := getDSN(t) 34 | 35 | db, err := sql.Open("postgres", pgDSN) 36 | require.NoError(t, err) 37 | 38 | defer func() { 39 | err := db.Close() 40 | assert.NoError(t, err) 41 | }() 42 | 43 | var initialConnections int 44 | row := db.QueryRow(`SELECT sum(numbackends) FROM pg_stat_database`) 45 | err = row.Scan(&initialConnections) 46 | require.NoError(t, err) 47 | 48 | check := New(Config{ 49 | DSN: pgDSN, 50 | }) 51 | 52 | ctx := context.Background() 53 | for i := 0; i < 10; i++ { 54 | err := check(ctx) 55 | assert.NoError(t, err) 56 | time.Sleep(100 * time.Millisecond) 57 | } 58 | 59 | var currentConnections int 60 | row = db.QueryRow(`SELECT sum(numbackends) FROM pg_stat_database`) 61 | err = row.Scan(¤tConnections) 62 | require.NoError(t, err) 63 | 64 | assert.Equal(t, initialConnections, currentConnections) 65 | } 66 | 67 | func getDSN(t *testing.T) string { 68 | t.Helper() 69 | 70 | pgDSN, ok := os.LookupEnv(pgDSNEnv) 71 | require.True(t, ok) 72 | 73 | // "docker compose port " returns 0.0.0.0:XXXX locally, change it to local port 74 | pgDSN = strings.Replace(pgDSN, "0.0.0.0:", "127.0.0.1:", 1) 75 | 76 | return pgDSN 77 | } 78 | 79 | var dbInit sync.Once 80 | 81 | func initDB(t *testing.T) { 82 | t.Helper() 83 | 84 | dbInit.Do(func() { 85 | db, err := sql.Open("postgres", getDSN(t)) 86 | require.NoError(t, err) 87 | 88 | defer func() { 89 | err := db.Close() 90 | assert.NoError(t, err) 91 | }() 92 | 93 | _, err = db.Exec(` 94 | CREATE TABLE IF NOT EXISTS test_pq ( 95 | id TEXT NOT NULL PRIMARY KEY, 96 | secret TEXT NOT NULL, 97 | extra TEXT NOT NULL, 98 | redirect_uri TEXT NOT NULL 99 | ); 100 | `) 101 | require.NoError(t, err) 102 | }) 103 | } 104 | -------------------------------------------------------------------------------- /checks/rabbitmq/aliveness_check_test.go: -------------------------------------------------------------------------------- 1 | package rabbitmq 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | 11 | "github.com/hellofresh/health-go/v5/checks/http" 12 | ) 13 | 14 | const httpURLEnv = "HEALTH_GO_MQ_URL" 15 | 16 | func TestAliveness(t *testing.T) { 17 | check := http.New(http.Config{ 18 | URL: getURL(t), 19 | }) 20 | 21 | err := check(context.Background()) 22 | require.NoError(t, err) 23 | } 24 | 25 | func getURL(t *testing.T) string { 26 | t.Helper() 27 | 28 | httpURL, ok := os.LookupEnv(httpURLEnv) 29 | require.True(t, ok) 30 | 31 | // "docker compose port " returns 0.0.0.0:XXXX locally, change it to local port 32 | httpURL = strings.Replace(httpURL, "0.0.0.0:", "127.0.0.1:", 1) 33 | 34 | return httpURL 35 | } 36 | -------------------------------------------------------------------------------- /checks/rabbitmq/check.go: -------------------------------------------------------------------------------- 1 | package rabbitmq 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "time" 8 | 9 | amqp "github.com/rabbitmq/amqp091-go" 10 | ) 11 | 12 | const ( 13 | defaultExchange = "health_check" 14 | defaultDialTimeout = 200 * time.Millisecond 15 | defaultConsumeTimeout = time.Second * 3 16 | ) 17 | 18 | type ( 19 | // Config is the RabbitMQ checker configuration settings container. 20 | Config struct { 21 | // DSN is the RabbitMQ instance connection DSN. Required. 22 | DSN string 23 | // Exchange is the application health check exchange. If not set - "health_check" is used. 24 | Exchange string 25 | // RoutingKey is the application health check routing key within health check exchange. 26 | // Can be an application or host name, for example. 27 | // If not set - host name is used. 28 | RoutingKey string 29 | // Queue is the application health check queue, that binds to the exchange with the routing key. 30 | // If not set - "." is used. 31 | Queue string 32 | // ConsumeTimeout is the duration that health check will try to consume published test message. 33 | // If not set - 3 seconds 34 | ConsumeTimeout time.Duration 35 | // DialTimeout is the duration that health check will try to dial to RabbitMQ. 36 | // If not set - 200 milliseconds. 37 | DialTimeout time.Duration 38 | } 39 | ) 40 | 41 | // New creates new RabbitMQ health check that verifies the following: 42 | // - connection establishing 43 | // - getting channel from the connection 44 | // - declaring topic exchange 45 | // - declaring queue 46 | // - binding a queue to the exchange with the defined routing key 47 | // - publishing a message to the exchange with the defined routing key 48 | // - consuming published message 49 | func New(config Config) func(ctx context.Context) error { 50 | (&config).defaults() 51 | 52 | return func(ctx context.Context) (checkErr error) { 53 | conn, err := amqp.DialConfig(config.DSN, amqp.Config{ 54 | Dial: amqp.DefaultDial(config.DialTimeout), 55 | }) 56 | if err != nil { 57 | checkErr = fmt.Errorf("RabbitMQ health check failed on dial phase: %w", err) 58 | return 59 | } 60 | defer func() { 61 | // override checkErr only if there were no other errors 62 | if err := conn.Close(); err != nil && checkErr == nil { 63 | checkErr = fmt.Errorf("RabbitMQ health check failed to close connection: %w", err) 64 | } 65 | }() 66 | 67 | ch, err := conn.Channel() 68 | if err != nil { 69 | checkErr = fmt.Errorf("RabbitMQ health check failed on getting channel phase: %w", err) 70 | return 71 | } 72 | defer func() { 73 | // override checkErr only if there were no other errors 74 | if err := ch.Close(); err != nil && checkErr == nil { 75 | checkErr = fmt.Errorf("RabbitMQ health check failed to close channel: %w", err) 76 | } 77 | }() 78 | 79 | if err := ch.ExchangeDeclare(config.Exchange, "topic", true, false, false, false, nil); err != nil { 80 | checkErr = fmt.Errorf("RabbitMQ health check failed during declaring exchange: %w", err) 81 | return 82 | } 83 | 84 | if _, err := ch.QueueDeclare(config.Queue, false, false, false, false, nil); err != nil { 85 | checkErr = fmt.Errorf("RabbitMQ health check failed during declaring queue: %w", err) 86 | return 87 | } 88 | 89 | if err := ch.QueueBind(config.Queue, config.RoutingKey, config.Exchange, false, nil); err != nil { 90 | checkErr = fmt.Errorf("RabbitMQ health check failed during binding: %w", err) 91 | return 92 | } 93 | 94 | messages, err := ch.Consume(config.Queue, "", true, false, false, false, nil) 95 | if err != nil { 96 | checkErr = fmt.Errorf("RabbitMQ health check failed during consuming: %w", err) 97 | return 98 | } 99 | 100 | done := make(chan struct{}) 101 | 102 | go func() { 103 | // block until: a message is received, or message channel is closed (consume timeout) 104 | <-messages 105 | 106 | // release the channel resources, and unblock the receive on done below 107 | close(done) 108 | }() 109 | 110 | p := amqp.Publishing{Body: []byte(time.Now().Format(time.RFC3339Nano))} 111 | if err := ch.Publish(config.Exchange, config.RoutingKey, false, false, p); err != nil { 112 | checkErr = fmt.Errorf("RabbitMQ health check failed during publishing: %w", err) 113 | return 114 | } 115 | 116 | for { 117 | select { 118 | case <-time.After(config.ConsumeTimeout): 119 | checkErr = fmt.Errorf("RabbitMQ health check failed due to consume timeout: %w", err) 120 | return 121 | case <-ctx.Done(): 122 | checkErr = fmt.Errorf("RabbitMQ health check failed due "+ 123 | "to health check listener disconnect: %w", ctx.Err()) 124 | return 125 | case <-done: 126 | return 127 | } 128 | } 129 | } 130 | } 131 | 132 | func (c *Config) defaults() { 133 | if c.Exchange == "" { 134 | c.Exchange = defaultExchange 135 | } 136 | 137 | if c.RoutingKey == "" { 138 | host, err := os.Hostname() 139 | if nil != err { 140 | c.RoutingKey = "-unknown-" 141 | } 142 | c.RoutingKey = host 143 | } 144 | 145 | if c.Queue == "" { 146 | c.Queue = fmt.Sprintf("%s.%s", c.Exchange, c.RoutingKey) 147 | } 148 | 149 | if c.ConsumeTimeout == 0 { 150 | c.ConsumeTimeout = defaultConsumeTimeout 151 | } 152 | if c.DialTimeout == 0 { 153 | c.DialTimeout = defaultDialTimeout 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /checks/rabbitmq/check_test.go: -------------------------------------------------------------------------------- 1 | package rabbitmq 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | const mqDSNEnv = "HEALTH_GO_MQ_DSN" 14 | 15 | func TestNew(t *testing.T) { 16 | check := New(Config{ 17 | DSN: getDSN(t), 18 | }) 19 | 20 | err := check(context.Background()) 21 | require.NoError(t, err) 22 | } 23 | 24 | func TestConfig(t *testing.T) { 25 | conf := &Config{ 26 | DSN: getDSN(t), 27 | } 28 | 29 | conf.defaults() 30 | 31 | assert.Equal(t, defaultExchange, conf.Exchange) 32 | assert.Equal(t, defaultConsumeTimeout, conf.ConsumeTimeout) 33 | } 34 | 35 | func getDSN(t *testing.T) string { 36 | t.Helper() 37 | 38 | mqDSN, ok := os.LookupEnv(mqDSNEnv) 39 | require.True(t, ok) 40 | 41 | // "docker compose port " returns 0.0.0.0:XXXX locally, change it to local port 42 | mqDSN = strings.Replace(mqDSN, "0.0.0.0:", "127.0.0.1:", 1) 43 | 44 | return mqDSN 45 | } 46 | -------------------------------------------------------------------------------- /checks/redis/check.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/redis/go-redis/v9" 9 | ) 10 | 11 | // Config is the Redis checker configuration settings container. 12 | type Config struct { 13 | // DSN is the Redis instance connection DSN. Required. 14 | DSN string 15 | } 16 | 17 | // New creates new Redis health check that verifies the following: 18 | // - connection establishing 19 | // - doing the PING command and verifying the response 20 | func New(config Config) func(ctx context.Context) error { 21 | // support all DSN formats (for backward compatibility) - with and w/out schema and path part: 22 | // - redis://localhost:1234/ 23 | // - rediss://localhost:1234/ 24 | // - localhost:1234 25 | redisDSN := config.DSN 26 | if !strings.HasPrefix(redisDSN, "redis://") && !strings.HasPrefix(redisDSN, "rediss://") { 27 | redisDSN = fmt.Sprintf("redis://%s", redisDSN) 28 | } 29 | redisOptions, _ := redis.ParseURL(redisDSN) 30 | 31 | return func(ctx context.Context) error { 32 | rdb := redis.NewClient(redisOptions) 33 | defer rdb.Close() 34 | 35 | pong, err := rdb.Ping(ctx).Result() 36 | if err != nil { 37 | return fmt.Errorf("redis ping failed: %w", err) 38 | } 39 | 40 | if pong != "PONG" { 41 | return fmt.Errorf("unexpected response for redis ping: %q", pong) 42 | } 43 | 44 | return nil 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /checks/redis/check_test.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | const rdDSNEnv = "HEALTH_GO_RD_DSN" 13 | 14 | func TestNew(t *testing.T) { 15 | check := New(Config{ 16 | DSN: getDSN(t), 17 | }) 18 | 19 | err := check(context.Background()) 20 | require.NoError(t, err) 21 | } 22 | 23 | func getDSN(t *testing.T) string { 24 | t.Helper() 25 | 26 | redisDSN, ok := os.LookupEnv(rdDSNEnv) 27 | require.True(t, ok) 28 | 29 | // "docker compose port " returns 0.0.0.0:XXXX locally, change it to local port 30 | redisDSN = strings.Replace(redisDSN, "0.0.0.0:", "127.0.0.1:", 1) 31 | 32 | return redisDSN 33 | } 34 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | services: 3 | 4 | pg-pq: 5 | image: postgres:10 6 | ports: 7 | - "5432" 8 | environment: 9 | POSTGRES_USER: test 10 | POSTGRES_PASSWORD: test 11 | POSTGRES_DB: test 12 | tmpfs: 13 | - /var/lib/postgresql/data 14 | healthcheck: 15 | test: [ "CMD", "pg_isready" ] 16 | interval: 10s 17 | timeout: 5s 18 | retries: 5 19 | 20 | pg-pgx4: 21 | image: postgres:10 22 | ports: 23 | - "5432" 24 | environment: 25 | POSTGRES_USER: test 26 | POSTGRES_PASSWORD: test 27 | POSTGRES_DB: test 28 | tmpfs: 29 | - /var/lib/postgresql/data 30 | healthcheck: 31 | test: [ "CMD", "pg_isready" ] 32 | interval: 10s 33 | timeout: 5s 34 | retries: 5 35 | 36 | pg-pgx5: 37 | image: postgres:10 38 | ports: 39 | - "5432" 40 | environment: 41 | POSTGRES_USER: test 42 | POSTGRES_PASSWORD: test 43 | POSTGRES_DB: test 44 | tmpfs: 45 | - /var/lib/postgresql/data 46 | healthcheck: 47 | test: [ "CMD", "pg_isready" ] 48 | interval: 10s 49 | timeout: 5s 50 | retries: 5 51 | 52 | rabbit: 53 | image: rabbitmq:3.6-management-alpine 54 | ports: 55 | - "5672" 56 | - "15672" 57 | healthcheck: 58 | test: [ "CMD", "rabbitmqctl", "status" ] 59 | interval: 10s 60 | timeout: 5s 61 | retries: 5 62 | 63 | redis: 64 | image: redis:3.2-alpine 65 | ports: 66 | - "6379" 67 | healthcheck: 68 | test: [ "CMD", "redis-cli", "ping" ] 69 | interval: 10s 70 | timeout: 5s 71 | retries: 5 72 | 73 | mongo: 74 | image: mongo:3 75 | ports: 76 | - "27017" 77 | tmpfs: 78 | - /var/lib/mongodb 79 | - /data/db/ 80 | healthcheck: 81 | test: "mongo localhost:27017/test --quiet --eval 'quit(db.runCommand({ ping: 1 }).ok ? 0 : 2)'" 82 | interval: 10s 83 | timeout: 5s 84 | retries: 5 85 | 86 | mysql: 87 | image: mysql:8 88 | ports: 89 | - "3306" 90 | environment: 91 | MYSQL_ROOT_PASSWORD: test 92 | MYSQL_DATABASE: test 93 | MYSQL_USER: test 94 | MYSQL_PASSWORD: test 95 | tmpfs: 96 | - /var/lib/mysql 97 | healthcheck: 98 | test: [ "CMD", "mysqladmin", "ping", "-h", "localhost" ] 99 | interval: 10s 100 | timeout: 5s 101 | retries: 5 102 | 103 | memcached: 104 | image: memcached:1.6.9-alpine 105 | ports: 106 | - "11211" 107 | 108 | influxdb: 109 | image: influxdb:1.8 110 | ports: 111 | - "8086" 112 | environment: 113 | DOCKER_INFLUXDB_INIT_USERNAME: test 114 | DOCKER_INFLUXDB_INIT_PASSWORD: test 115 | DOCKER_INFLUXDB_INIT_ORG: test 116 | DOCKER_INFLUXDB_INIT_BUCKET: test 117 | 118 | cassandra: 119 | image: cassandra:4.1.0 120 | ports: 121 | - "9042" 122 | healthcheck: 123 | test: [ "CMD", "cqlsh", "-u cassandra", "-p cassandra" ,"-e describe keyspaces" ] 124 | interval: 15s 125 | timeout: 10s 126 | retries: 10 127 | 128 | nats: 129 | container_name: nats 130 | image: nats:2.9.11 131 | command: "-js -sd /data" 132 | ports: 133 | - "4222:4222" 134 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Health-Go 2 | 3 | ## Functionality 4 | 5 | * Exposes an HTTP handler that retrieves health status of the application 6 | * Implements some generic checkers for the following services: 7 | * RabbitMQ 8 | * PostgreSQL 9 | * Redis 10 | * HTTP 11 | * MongoDB 12 | * MySQL 13 | * gRPC 14 | * Memcached 15 | * Nats 16 | 17 | ## Usage 18 | 19 | The library exports `Handler` and `HandlerFunc` functions which are fully compatible with `net/http`. 20 | 21 | Additionally, library exports `Measure` function that returns summary status for all the registered health checks, 22 | so it can be used in non-HTTP environments. 23 | 24 | ### Handler 25 | 26 | ```go 27 | package main 28 | 29 | import ( 30 | "context" 31 | "net/http" 32 | "time" 33 | 34 | "github.com/hellofresh/health-go/v5" 35 | healthMysql "github.com/hellofresh/health-go/v5/checks/mysql" 36 | ) 37 | 38 | func main() { 39 | // add some checks on instance creation 40 | h, _ := health.New(health.WithChecks(health.Config{ 41 | Name: "rabbitmq", 42 | Timeout: time.Second * 5, 43 | SkipOnErr: true, 44 | Check: func(ctx context.Context) error { 45 | // rabbitmq health check implementation goes here 46 | return nil 47 | }}, health.Config{ 48 | Name: "mongodb", 49 | Check: func(ctx context.Context) error { 50 | // mongo_db health check implementation goes here 51 | return nil 52 | }, 53 | }, 54 | )) 55 | 56 | // and then add some more if needed 57 | h.Register(health.Config{ 58 | Name: "mysql", 59 | Timeout: time.Second * 2, 60 | SkipOnErr: false, 61 | Check: healthMysql.New(healthMysql.Config{ 62 | DSN: "test:test@tcp(0.0.0.0:31726)/test?charset=utf8", 63 | }), 64 | }) 65 | 66 | http.Handle("/status", h.Handler()) 67 | http.ListenAndServe(":3000", nil) 68 | } 69 | ``` 70 | 71 | ### HandlerFunc 72 | ```go 73 | package main 74 | 75 | import ( 76 | "context" 77 | "net/http" 78 | "time" 79 | 80 | "github.com/go-chi/chi" 81 | "github.com/hellofresh/health-go/v5" 82 | healthMysql "github.com/hellofresh/health-go/v5/checks/mysql" 83 | ) 84 | 85 | func main() { 86 | // add some checks on instance creation 87 | h, _ := health.New(health.WithChecks(health.Config{ 88 | Name: "rabbitmq", 89 | Timeout: time.Second * 5, 90 | SkipOnErr: true, 91 | Check: func(ctx context.Context) error { 92 | // rabbitmq health check implementation goes here 93 | return nil 94 | }}, health.Config{ 95 | Name: "mongodb", 96 | Check: func(ctx context.Context) error { 97 | // mongo_db health check implementation goes here 98 | return nil 99 | }, 100 | }, 101 | )) 102 | 103 | // and then add some more if needed 104 | h.Register(health.Config{ 105 | Name: "mysql", 106 | Timeout: time.Second * 2, 107 | SkipOnErr: false, 108 | Check: healthMysql.New(healthMysql.Config{ 109 | DSN: "test:test@tcp(0.0.0.0:31726)/test?charset=utf8", 110 | }), 111 | }) 112 | 113 | r := chi.NewRouter() 114 | r.Get("/status", h.HandlerFunc) 115 | http.ListenAndServe(":3000", nil) 116 | } 117 | ``` 118 | 119 | For more examples please check [here](https://github.com/hellofresh/health-go/blob/master/_examples/server.go) 120 | 121 | ## API Documentation 122 | 123 | ### `GET /status` 124 | 125 | Get the health of the application. 126 | 127 | - Method: `GET` 128 | - Endpoint: `/status` 129 | - Request: 130 | ``` 131 | curl localhost:3000/status 132 | ``` 133 | - Response: 134 | 135 | HTTP/1.1 200 OK 136 | ```json 137 | { 138 | "status": "OK", 139 | "timestamp": "2017-01-01T00:00:00.413567856+033:00", 140 | "system": { 141 | "version": "go1.8", 142 | "goroutines_count": 4, 143 | "total_alloc_bytes": 21321, 144 | "heap_objects_count": 21323, 145 | "alloc_bytes": 234523 146 | } 147 | } 148 | ``` 149 | 150 | HTTP/1.1 200 OK 151 | ```json 152 | { 153 | "status": "Partially Available", 154 | "timestamp": "2017-01-01T00:00:00.413567856+033:00", 155 | "failures": { 156 | "rabbitmq": "Failed during rabbitmq health check" 157 | }, 158 | "system": { 159 | "version": "go1.8", 160 | "goroutines_count": 4, 161 | "total_alloc_bytes": 21321, 162 | "heap_objects_count": 21323, 163 | "alloc_bytes": 234523 164 | } 165 | } 166 | ``` 167 | 168 | HTTP/1.1 503 Service Unavailable 169 | ```json 170 | { 171 | "status": "Unavailable", 172 | "timestamp": "2017-01-01T00:00:00.413567856+033:00", 173 | "failures": { 174 | "mongodb": "Failed during mongodb health check" 175 | }, 176 | "system": { 177 | "version": "go1.8", 178 | "goroutines_count": 4, 179 | "total_alloc_bytes": 21321, 180 | "heap_objects_count": 21323, 181 | "alloc_bytes": 234523 182 | } 183 | } 184 | ``` 185 | 186 | ## Contributing 187 | 188 | - Fork it 189 | - Create your feature branch (`git checkout -b my-new-feature`) 190 | - Commit your changes (`git commit -am 'Add some feature'`) 191 | - Push to the branch (`git push origin my-new-feature`) 192 | - Create new Pull Request 193 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hellofresh/health-go/v5 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.9 6 | 7 | require ( 8 | github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 9 | github.com/go-sql-driver/mysql v1.7.1 10 | github.com/gocql/gocql v1.6.0 11 | github.com/influxdata/influxdb-client-go/v2 v2.13.0 12 | github.com/jackc/pgx/v4 v4.18.3 13 | github.com/jackc/pgx/v5 v5.5.5 14 | github.com/lib/pq v1.10.9 15 | github.com/nats-io/nats.go v1.33.1 16 | github.com/rabbitmq/amqp091-go v1.9.0 17 | github.com/redis/go-redis/v9 v9.5.1 18 | github.com/stretchr/testify v1.10.0 19 | github.com/vitorsalgado/mocha/v2 v2.0.2 20 | go.mongodb.org/mongo-driver v1.14.0 21 | go.opentelemetry.io/otel v1.35.0 22 | go.opentelemetry.io/otel/trace v1.35.0 23 | google.golang.org/grpc v1.62.1 24 | ) 25 | 26 | require ( 27 | github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect 28 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 29 | github.com/davecgh/go-spew v1.1.1 // indirect 30 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 31 | github.com/golang/protobuf v1.5.3 // indirect 32 | github.com/golang/snappy v0.0.4 // indirect 33 | github.com/google/uuid v1.6.0 // indirect 34 | github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect 35 | github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf // indirect 36 | github.com/jackc/chunkreader/v2 v2.0.1 // indirect 37 | github.com/jackc/pgconn v1.14.3 // indirect 38 | github.com/jackc/pgio v1.0.0 // indirect 39 | github.com/jackc/pgpassfile v1.0.0 // indirect 40 | github.com/jackc/pgproto3/v2 v2.3.3 // indirect 41 | github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect 42 | github.com/jackc/pgtype v1.14.0 // indirect 43 | github.com/klauspost/compress v1.17.4 // indirect 44 | github.com/montanaflynn/stats v0.7.1 // indirect 45 | github.com/nats-io/nkeys v0.4.7 // indirect 46 | github.com/nats-io/nuid v1.0.1 // indirect 47 | github.com/oapi-codegen/runtime v1.1.0 // indirect 48 | github.com/pkg/errors v0.9.1 // indirect 49 | github.com/pmezard/go-difflib v1.0.0 // indirect 50 | github.com/rogpeppe/go-internal v1.10.0 // indirect 51 | github.com/stretchr/objx v0.5.2 // indirect 52 | github.com/xdg-go/pbkdf2 v1.0.0 // indirect 53 | github.com/xdg-go/scram v1.1.2 // indirect 54 | github.com/xdg-go/stringprep v1.0.4 // indirect 55 | github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect 56 | golang.org/x/crypto v0.35.0 // indirect 57 | golang.org/x/net v0.33.0 // indirect 58 | golang.org/x/sync v0.11.0 // indirect 59 | golang.org/x/sys v0.30.0 // indirect 60 | golang.org/x/text v0.22.0 // indirect 61 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 // indirect 62 | google.golang.org/protobuf v1.33.0 // indirect 63 | gopkg.in/inf.v0 v0.9.1 // indirect 64 | gopkg.in/yaml.v3 v3.0.1 // indirect 65 | ) 66 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= 3 | github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= 4 | github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= 5 | github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= 6 | github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY= 7 | github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k= 8 | github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= 9 | github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= 10 | github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= 11 | github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 h1:N7oVaKyGp8bttX0bfZGmcGkjz7DLQXhAn3DNd3T0ous= 12 | github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c= 13 | github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= 14 | github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= 15 | github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= 16 | github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= 17 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 18 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 19 | github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= 20 | github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= 21 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 22 | github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 23 | github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= 24 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 25 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 26 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 27 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 28 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 29 | github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= 30 | github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= 31 | github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= 32 | github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= 33 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 34 | github.com/gocql/gocql v1.6.0 h1:IdFdOTbnpbd0pDhl4REKQDM+Q0SzKXQ1Yh+YZZ8T/qU= 35 | github.com/gocql/gocql v1.6.0/go.mod h1:3gM2c4D3AnkISwBxGnMMsS8Oy4y2lhbPRsH4xnJrHG8= 36 | github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= 37 | github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 38 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 39 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 40 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 41 | github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 42 | github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= 43 | github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 44 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 45 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 46 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 47 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 48 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 49 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 50 | github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= 51 | github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= 52 | github.com/influxdata/influxdb-client-go/v2 v2.13.0 h1:ioBbLmR5NMbAjP4UVA5r9b5xGjpABD7j65pI8kFphDM= 53 | github.com/influxdata/influxdb-client-go/v2 v2.13.0/go.mod h1:k+spCbt9hcvqvUiz0sr5D8LolXHqAAOfPw9v/RIRHl4= 54 | github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf h1:7JTmneyiNEwVBOHSjoMxiWAqB992atOeepeFYegn5RU= 55 | github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= 56 | github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= 57 | github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= 58 | github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= 59 | github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= 60 | github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= 61 | github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= 62 | github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= 63 | github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= 64 | github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= 65 | github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= 66 | github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w= 67 | github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM= 68 | github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= 69 | github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= 70 | github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= 71 | github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= 72 | github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= 73 | github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= 74 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 75 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 76 | github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= 77 | github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= 78 | github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= 79 | github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= 80 | github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= 81 | github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 82 | github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 83 | github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag= 84 | github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 85 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= 86 | github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA= 87 | github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 88 | github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= 89 | github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= 90 | github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= 91 | github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= 92 | github.com/jackc/pgtype v1.14.0 h1:y+xUdabmyMkJLyApYuPj38mW+aAIqCe5uuBB51rH3Vw= 93 | github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= 94 | github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= 95 | github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= 96 | github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= 97 | github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= 98 | github.com/jackc/pgx/v4 v4.18.3 h1:dE2/TrEsGX3RBprb3qryqSV9Y60iZN1C6i8IrmW9/BA= 99 | github.com/jackc/pgx/v4 v4.18.3/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= 100 | github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= 101 | github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= 102 | github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 103 | github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 104 | github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 105 | github.com/jackc/puddle v1.3.0 h1:eHK/5clGOatcjX3oWGBO/MpxpbHzSwud5EWTSCI+MX0= 106 | github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= 107 | github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= 108 | github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= 109 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 110 | github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= 111 | github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= 112 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 113 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 114 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 115 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 116 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 117 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 118 | github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= 119 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 120 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 121 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 122 | github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 123 | github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 124 | github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 125 | github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 126 | github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 127 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 128 | github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= 129 | github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 130 | github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 131 | github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 132 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 133 | github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= 134 | github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= 135 | github.com/nats-io/nats.go v1.33.1 h1:8TxLZZ/seeEfR97qV0/Bl939tpDnt2Z2fK3HkPypj70= 136 | github.com/nats-io/nats.go v1.33.1/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8= 137 | github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI= 138 | github.com/nats-io/nkeys v0.4.7/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDmGD0nc= 139 | github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= 140 | github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= 141 | github.com/oapi-codegen/runtime v1.1.0 h1:rJpoNUawn5XTvekgfkvSZr0RqEnoYpFkyvrzfWeFKWM= 142 | github.com/oapi-codegen/runtime v1.1.0/go.mod h1:BeSfBkWWWnAnGdyS+S/GnlbmHKzf8/hwkvelJZDeKA8= 143 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 144 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 145 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 146 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 147 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 148 | github.com/rabbitmq/amqp091-go v1.9.0 h1:qrQtyzB4H8BQgEuJwhmVQqVHB9O4+MNDJCCAcpc3Aoo= 149 | github.com/rabbitmq/amqp091-go v1.9.0/go.mod h1:+jPrT9iY2eLjRaMSRHUhc3z14E/l85kv/f+6luSD3pc= 150 | github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8= 151 | github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= 152 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 153 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 154 | github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 155 | github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= 156 | github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= 157 | github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= 158 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 159 | github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= 160 | github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= 161 | github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 162 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 163 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 164 | github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= 165 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 166 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 167 | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 168 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 169 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 170 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 171 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 172 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 173 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 174 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 175 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 176 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 177 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 178 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 179 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 180 | github.com/vitorsalgado/mocha/v2 v2.0.2 h1:wb1QCRzVkp8uhRcUYmb9jJfbMj/qbiqcDyD8rD+Ldfw= 181 | github.com/vitorsalgado/mocha/v2 v2.0.2/go.mod h1:l7jRVm7KTL4VAxxazH99UVo+KzwztjrYpFTksTmL1DE= 182 | github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= 183 | github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= 184 | github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= 185 | github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= 186 | github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= 187 | github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= 188 | github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk= 189 | github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4= 190 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 191 | github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= 192 | go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80= 193 | go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= 194 | go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= 195 | go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= 196 | go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= 197 | go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= 198 | go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 199 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 200 | go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 201 | go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 202 | go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= 203 | go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= 204 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 205 | go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= 206 | go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 207 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 208 | go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 209 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 210 | go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= 211 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 212 | golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= 213 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 214 | golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 215 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 216 | golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 217 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 218 | golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= 219 | golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 220 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 221 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 222 | golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= 223 | golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= 224 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 225 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 226 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 227 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 228 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 229 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 230 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 231 | golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 232 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 233 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 234 | golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= 235 | golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= 236 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 237 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 238 | golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= 239 | golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 240 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 241 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 242 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 243 | golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 244 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 245 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 246 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 247 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 248 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 249 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 250 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 251 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 252 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 253 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 254 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 255 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 256 | golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 257 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 258 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 259 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 260 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 261 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 262 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 263 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 264 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 265 | golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 266 | golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= 267 | golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= 268 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 269 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 270 | golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 271 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 272 | golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 273 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 274 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 275 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 276 | golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 277 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 278 | golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 279 | golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 280 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 281 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 282 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 283 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 284 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 h1:AjyfHzEPEFp/NpvfN5g+KDla3EMojjhRVZc1i7cj+oM= 285 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80/go.mod h1:PAREbraiVEVGVdTZsVWjSbbTtSyGbAgIIvni8a8CD5s= 286 | google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk= 287 | google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE= 288 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 289 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 290 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 291 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 292 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 293 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 294 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 295 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 296 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 297 | gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= 298 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 299 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 300 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 301 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 302 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 303 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 304 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 305 | -------------------------------------------------------------------------------- /health.go: -------------------------------------------------------------------------------- 1 | package health 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "net/http" 9 | "runtime" 10 | "sync" 11 | "time" 12 | 13 | "go.opentelemetry.io/otel/attribute" 14 | "go.opentelemetry.io/otel/codes" 15 | "go.opentelemetry.io/otel/trace" 16 | ) 17 | 18 | // Status type represents health status 19 | type Status string 20 | 21 | // Possible health statuses 22 | const ( 23 | StatusOK Status = "OK" 24 | StatusPartiallyAvailable Status = "Partially Available" 25 | StatusUnavailable Status = "Unavailable" 26 | StatusTimeout Status = "Timeout during health check" 27 | ) 28 | 29 | type ( 30 | // CheckFunc is the func which executes the check. 31 | CheckFunc func(context.Context) error 32 | 33 | // Config carries the parameters to run the check. 34 | Config struct { 35 | // Name is the name of the resource to be checked. 36 | Name string 37 | // Timeout is the timeout defined for every check. 38 | Timeout time.Duration 39 | // SkipOnErr if set to true, it will retrieve StatusOK providing the error message from the failed resource. 40 | SkipOnErr bool 41 | // Check is the func which executes the check. 42 | Check CheckFunc 43 | } 44 | 45 | // Check represents the health check response. 46 | Check struct { 47 | // Status is the check status. 48 | Status Status `json:"status"` 49 | // Timestamp is the time in which the check occurred. 50 | Timestamp time.Time `json:"timestamp"` 51 | // Failures holds the failed checks along with their messages. 52 | Failures map[string]string `json:"failures,omitempty"` 53 | // System holds information of the go process. 54 | *System `json:"system,omitempty"` 55 | // Component holds information on the component for which checks are made 56 | Component `json:"component"` 57 | } 58 | 59 | // System runtime variables about the go process. 60 | System struct { 61 | // Version is the go version. 62 | Version string `json:"version"` 63 | // GoroutinesCount is the number of the current goroutines. 64 | GoroutinesCount int `json:"goroutines_count"` 65 | // TotalAllocBytes is the total bytes allocated. 66 | TotalAllocBytes int `json:"total_alloc_bytes"` 67 | // HeapObjectsCount is the number of objects in the go heap. 68 | HeapObjectsCount int `json:"heap_objects_count"` 69 | // TotalAllocBytes is the bytes allocated and not yet freed. 70 | AllocBytes int `json:"alloc_bytes"` 71 | } 72 | 73 | // Component descriptive values about the component for which checks are made 74 | Component struct { 75 | // Name is the name of the component. 76 | Name string `json:"name"` 77 | // Version is the component version. 78 | Version string `json:"version"` 79 | } 80 | 81 | // Health is the health-checks container 82 | Health struct { 83 | mu sync.Mutex 84 | checks map[string]Config 85 | maxConcurrent int 86 | 87 | tp trace.TracerProvider 88 | instrumentationName string 89 | 90 | component Component 91 | 92 | systemInfoEnabled bool 93 | } 94 | ) 95 | 96 | // New instantiates and build new health check container 97 | func New(opts ...Option) (*Health, error) { 98 | h := &Health{ 99 | checks: make(map[string]Config), 100 | tp: trace.NewNoopTracerProvider(), 101 | maxConcurrent: runtime.NumCPU(), 102 | } 103 | 104 | for _, o := range opts { 105 | if err := o(h); err != nil { 106 | return nil, err 107 | } 108 | } 109 | 110 | return h, nil 111 | } 112 | 113 | // Register registers a check config to be performed. 114 | func (h *Health) Register(c Config) error { 115 | if c.Timeout == 0 { 116 | c.Timeout = time.Second * 2 117 | } 118 | 119 | if c.Name == "" { 120 | return errors.New("health check must have a name to be registered") 121 | } 122 | 123 | h.mu.Lock() 124 | defer h.mu.Unlock() 125 | 126 | if _, ok := h.checks[c.Name]; ok { 127 | return fmt.Errorf("health check %q is already registered", c.Name) 128 | } 129 | 130 | h.checks[c.Name] = c 131 | 132 | return nil 133 | } 134 | 135 | // Handler returns an HTTP handler (http.HandlerFunc). 136 | func (h *Health) Handler() http.Handler { 137 | return http.HandlerFunc(h.HandlerFunc) 138 | } 139 | 140 | // HandlerFunc is the HTTP handler function. 141 | func (h *Health) HandlerFunc(w http.ResponseWriter, r *http.Request) { 142 | c := h.Measure(r.Context()) 143 | 144 | w.Header().Set("Content-Type", "application/json") 145 | data, err := json.Marshal(c) 146 | if err != nil { 147 | w.WriteHeader(http.StatusInternalServerError) 148 | http.Error(w, err.Error(), http.StatusInternalServerError) 149 | return 150 | } 151 | 152 | code := http.StatusOK 153 | if c.Status == StatusUnavailable { 154 | code = http.StatusServiceUnavailable 155 | } 156 | w.WriteHeader(code) 157 | w.Write(data) 158 | } 159 | 160 | // Measure runs all the registered health checks and returns summary status 161 | func (h *Health) Measure(ctx context.Context) Check { 162 | h.mu.Lock() 163 | defer h.mu.Unlock() 164 | 165 | tracer := h.tp.Tracer(h.instrumentationName) 166 | 167 | ctx, span := tracer.Start( 168 | ctx, 169 | "health.Measure", 170 | trace.WithAttributes(attribute.Int("checks", len(h.checks))), 171 | ) 172 | defer span.End() 173 | 174 | status := StatusOK 175 | failures := make(map[string]string) 176 | 177 | limiterCh := make(chan bool, h.maxConcurrent) 178 | defer close(limiterCh) 179 | 180 | var ( 181 | wg sync.WaitGroup 182 | mu sync.Mutex 183 | ) 184 | for _, c := range h.checks { 185 | limiterCh <- true 186 | wg.Add(1) 187 | 188 | go func(c Config) { 189 | ctx, span := tracer.Start(ctx, c.Name) 190 | defer func() { 191 | span.End() 192 | <-limiterCh 193 | wg.Done() 194 | }() 195 | 196 | resCh := make(chan error) 197 | 198 | go func() { 199 | resCh <- c.Check(ctx) 200 | defer close(resCh) 201 | }() 202 | 203 | timeout := time.NewTimer(c.Timeout) 204 | 205 | select { 206 | case <-timeout.C: 207 | mu.Lock() 208 | defer mu.Unlock() 209 | 210 | span.SetStatus(codes.Error, string(StatusTimeout)) 211 | 212 | failures[c.Name] = string(StatusTimeout) 213 | status = getAvailability(status, c.SkipOnErr) 214 | case res := <-resCh: 215 | if !timeout.Stop() { 216 | <-timeout.C 217 | } 218 | 219 | mu.Lock() 220 | defer mu.Unlock() 221 | 222 | if res != nil { 223 | span.RecordError(res) 224 | 225 | failures[c.Name] = res.Error() 226 | status = getAvailability(status, c.SkipOnErr) 227 | } 228 | } 229 | }(c) 230 | } 231 | 232 | wg.Wait() 233 | span.SetAttributes(attribute.String("status", string(status))) 234 | 235 | var systemMetrics *System 236 | if h.systemInfoEnabled { 237 | systemMetrics = newSystemMetrics() 238 | } 239 | 240 | return newCheck(h.component, status, systemMetrics, failures) 241 | } 242 | 243 | func newCheck(c Component, s Status, system *System, failures map[string]string) Check { 244 | return Check{ 245 | Status: s, 246 | Timestamp: time.Now(), 247 | Failures: failures, 248 | System: system, 249 | Component: c, 250 | } 251 | } 252 | 253 | func newSystemMetrics() *System { 254 | s := runtime.MemStats{} 255 | runtime.ReadMemStats(&s) 256 | 257 | return &System{ 258 | Version: runtime.Version(), 259 | GoroutinesCount: runtime.NumGoroutine(), 260 | TotalAllocBytes: int(s.TotalAlloc), 261 | HeapObjectsCount: int(s.HeapObjects), 262 | AllocBytes: int(s.Alloc), 263 | } 264 | } 265 | 266 | func getAvailability(s Status, skipOnErr bool) Status { 267 | if skipOnErr && s != StatusUnavailable { 268 | return StatusPartiallyAvailable 269 | } 270 | 271 | return StatusUnavailable 272 | } 273 | -------------------------------------------------------------------------------- /health_test.go: -------------------------------------------------------------------------------- 1 | package health 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "net/http" 8 | "net/http/httptest" 9 | "testing" 10 | "time" 11 | 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | const ( 17 | checkErr = "failed during RabbitMQ health check" 18 | ) 19 | 20 | func TestRegisterWithNoName(t *testing.T) { 21 | h, err := New() 22 | require.NoError(t, err) 23 | 24 | err = h.Register(Config{ 25 | Name: "", 26 | Check: func(context.Context) error { 27 | return nil 28 | }, 29 | }) 30 | require.Error(t, err, "health check registration with empty name should return an error") 31 | } 32 | 33 | func TestDoubleRegister(t *testing.T) { 34 | h, err := New() 35 | require.NoError(t, err) 36 | 37 | healthCheckName := "health-check" 38 | 39 | conf := Config{ 40 | Name: healthCheckName, 41 | Check: func(context.Context) error { 42 | return nil 43 | }, 44 | } 45 | 46 | err = h.Register(conf) 47 | require.NoError(t, err, "the first registration of a health check should not return an error, but got one") 48 | 49 | err = h.Register(conf) 50 | assert.Error(t, err, "the second registration of a health check config should return an error, but did not") 51 | 52 | err = h.Register(Config{ 53 | Name: healthCheckName, 54 | Check: func(context.Context) error { 55 | return errors.New("health checks registered") 56 | }, 57 | }) 58 | assert.Error(t, err, "registration with same name, but different details should still return an error, but did not") 59 | } 60 | 61 | func TestHealthHandler(t *testing.T) { 62 | h, err := New() 63 | require.NoError(t, err) 64 | 65 | res := httptest.NewRecorder() 66 | req, err := http.NewRequest("GET", "http://localhost/status", nil) 67 | require.NoError(t, err) 68 | 69 | err = h.Register(Config{ 70 | Name: "rabbitmq", 71 | SkipOnErr: true, 72 | Check: func(context.Context) error { return errors.New(checkErr) }, 73 | }) 74 | require.NoError(t, err) 75 | 76 | err = h.Register(Config{ 77 | Name: "mongodb", 78 | Check: func(context.Context) error { return nil }, 79 | }) 80 | require.NoError(t, err) 81 | 82 | err = h.Register(Config{ 83 | Name: "snail-service", 84 | SkipOnErr: true, 85 | Timeout: time.Second * 1, 86 | Check: func(context.Context) error { 87 | time.Sleep(time.Second * 2) 88 | return nil 89 | }, 90 | }) 91 | require.NoError(t, err) 92 | 93 | handler := h.Handler() 94 | handler.ServeHTTP(res, req) 95 | 96 | assert.Equal(t, http.StatusOK, res.Code, "status handler returned wrong status code") 97 | 98 | body := make(map[string]interface{}) 99 | err = json.NewDecoder(res.Body).Decode(&body) 100 | require.NoError(t, err) 101 | 102 | assert.Equal(t, string(StatusPartiallyAvailable), body["status"], "body returned wrong status") 103 | 104 | failure, ok := body["failures"] 105 | assert.True(t, ok, "body returned nil failures field") 106 | 107 | f, ok := failure.(map[string]interface{}) 108 | assert.True(t, ok, "body returned nil failures.rabbitmq field") 109 | 110 | assert.Equal(t, checkErr, f["rabbitmq"], "body returned wrong status for rabbitmq") 111 | assert.Equal(t, string(StatusTimeout), f["snail-service"], "body returned wrong status for snail-service") 112 | } 113 | 114 | func TestHealth_Measure(t *testing.T) { 115 | h, err := New(WithChecks(Config{ 116 | Name: "check1", 117 | Timeout: time.Second, 118 | SkipOnErr: false, 119 | Check: func(context.Context) error { 120 | time.Sleep(time.Second * 10) 121 | return errors.New("check1") 122 | }, 123 | }, Config{ 124 | Name: "check2", 125 | Timeout: time.Second * 2, 126 | SkipOnErr: false, 127 | Check: func(context.Context) error { 128 | time.Sleep(time.Second * 10) 129 | return errors.New("check2") 130 | }, 131 | }), WithMaxConcurrent(2)) 132 | require.NoError(t, err) 133 | 134 | startedAt := time.Now() 135 | result := h.Measure(context.Background()) 136 | elapsed := time.Since(startedAt) 137 | 138 | // both checks should run concurrently and should fail with timeout, 139 | // so should take not less than 2 sec, but less than 5 that is sequential check time 140 | require.GreaterOrEqual(t, elapsed.Milliseconds(), (time.Second * 2).Milliseconds()) 141 | require.Less(t, elapsed.Milliseconds(), (time.Second * 5).Milliseconds()) 142 | 143 | assert.Equal(t, StatusUnavailable, result.Status) 144 | assert.Equal(t, string(StatusTimeout), result.Failures["check1"]) 145 | assert.Equal(t, string(StatusTimeout), result.Failures["check2"]) 146 | assert.Nil(t, result.System) 147 | 148 | h, err = New(WithSystemInfo()) 149 | require.NoError(t, err) 150 | result = h.Measure(context.Background()) 151 | 152 | assert.NotNil(t, result.System) 153 | } 154 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: 'Health-Go' 2 | site_description: 'Main documentation and guide for Health-Go' 3 | 4 | plugins: 5 | - techdocs-core 6 | 7 | markdown_extensions: 8 | - admonition 9 | 10 | nav: 11 | - Home: index.md 12 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package health 2 | 3 | import ( 4 | "fmt" 5 | 6 | "go.opentelemetry.io/otel/trace" 7 | ) 8 | 9 | // Option is the health-container options type 10 | type Option func(*Health) error 11 | 12 | // WithChecks adds checks to newly instantiated health-container 13 | func WithChecks(checks ...Config) Option { 14 | return func(h *Health) error { 15 | for _, c := range checks { 16 | if err := h.Register(c); err != nil { 17 | return fmt.Errorf("could not register check %q: %w", c.Name, err) 18 | } 19 | } 20 | 21 | return nil 22 | } 23 | } 24 | 25 | // WithTracerProvider sets trace provider for the checks and instrumentation name that will be used 26 | // for tracer from trace provider. 27 | func WithTracerProvider(tp trace.TracerProvider, instrumentationName string) Option { 28 | return func(h *Health) error { 29 | h.tp = tp 30 | h.instrumentationName = instrumentationName 31 | 32 | return nil 33 | } 34 | } 35 | 36 | // WithComponent sets the component description of the component to which this check refer 37 | func WithComponent(component Component) Option { 38 | return func(h *Health) error { 39 | h.component = component 40 | 41 | return nil 42 | } 43 | } 44 | 45 | // WithMaxConcurrent sets max number of concurrently running checks. 46 | // Set to 1 if want to run all checks sequentially. 47 | func WithMaxConcurrent(n int) Option { 48 | return func(h *Health) error { 49 | h.maxConcurrent = n 50 | return nil 51 | } 52 | } 53 | 54 | // WithSystemInfo enables the option to return system information about the go process. 55 | func WithSystemInfo() Option { 56 | return func(h *Health) error { 57 | h.systemInfoEnabled = true 58 | return nil 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /options_test.go: -------------------------------------------------------------------------------- 1 | package health 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/mock" 10 | "github.com/stretchr/testify/require" 11 | "go.opentelemetry.io/otel/trace" 12 | "go.opentelemetry.io/otel/trace/embedded" 13 | ) 14 | 15 | func TestWithChecks(t *testing.T) { 16 | h1, err := New() 17 | require.NoError(t, err) 18 | assert.Len(t, h1.checks, 0) 19 | 20 | h2, err := New(WithChecks(Config{ 21 | Name: "foo", 22 | }, Config{ 23 | Name: "bar", 24 | })) 25 | require.NoError(t, err) 26 | assert.Len(t, h2.checks, 2) 27 | 28 | _, err = New(WithChecks(Config{ 29 | Name: "foo", 30 | }, Config{ 31 | Name: "foo", 32 | })) 33 | require.Error(t, err) 34 | } 35 | 36 | type mockTracerProvider struct { 37 | mock.Mock 38 | embedded.TracerProvider 39 | } 40 | 41 | func (m *mockTracerProvider) Tracer(instrumentationName string, opts ...trace.TracerOption) trace.Tracer { 42 | args := m.Called(instrumentationName, opts) 43 | return args.Get(0).(trace.Tracer) 44 | } 45 | 46 | func TestWithTracerProvider(t *testing.T) { 47 | h1, err := New() 48 | require.NoError(t, err) 49 | assert.Equal(t, "trace.noopTracerProvider", fmt.Sprintf("%T", h1.tp)) 50 | assert.Equal(t, "", h1.instrumentationName) 51 | 52 | tp := new(mockTracerProvider) 53 | instrumentationName := "test.test" 54 | 55 | h2, err := New(WithTracerProvider(tp, instrumentationName)) 56 | require.NoError(t, err) 57 | assert.Same(t, tp, h2.tp) 58 | assert.Equal(t, instrumentationName, h2.instrumentationName) 59 | } 60 | 61 | func TestWithComponent(t *testing.T) { 62 | h1, err := New() 63 | require.NoError(t, err) 64 | assert.Empty(t, h1.component.Name) 65 | assert.Empty(t, h1.component.Version) 66 | 67 | c := Component{ 68 | Name: "test", 69 | Version: "1.0", 70 | } 71 | 72 | h2, err := New(WithComponent(c)) 73 | require.NoError(t, err) 74 | assert.Equal(t, "test", h2.component.Name) 75 | assert.Equal(t, "1.0", h2.component.Version) 76 | } 77 | 78 | func TestWithMaxConcurrent(t *testing.T) { 79 | numCPU := runtime.NumCPU() 80 | t.Logf("Num CPUs: %d", numCPU) 81 | 82 | h1, err := New() 83 | require.NoError(t, err) 84 | assert.Equal(t, numCPU, h1.maxConcurrent) 85 | 86 | h2, err := New(WithMaxConcurrent(13)) 87 | require.NoError(t, err) 88 | assert.Equal(t, 13, h2.maxConcurrent) 89 | } 90 | 91 | func TestWithSystemInfo(t *testing.T) { 92 | h1, err := New() 93 | require.NoError(t, err) 94 | assert.False(t, h1.systemInfoEnabled) 95 | 96 | h2, err := New(WithSystemInfo()) 97 | require.NoError(t, err) 98 | assert.True(t, h2.systemInfoEnabled) 99 | } 100 | --------------------------------------------------------------------------------