├── .github └── workflows │ └── ci.yml ├── .golangci.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── TODO.md ├── assets ├── always-on-mechanism.png ├── consumer-safeguard.png ├── definitions.example.json ├── gorabbit-logo-lg.jpg ├── gorabbit-logo-md.jpg ├── gorabbit-logo-sm.jpg └── publishing-safeguard.png ├── channel.go ├── client.go ├── client_options.go ├── connection.go ├── connection_manager.go ├── constants.go ├── consumer.go ├── consumer_test.go ├── go.mod ├── go.sum ├── logger.go ├── manager.go ├── manager_options.go ├── manager_test.go ├── marshalling.go ├── marshalling_test.go ├── model.go ├── ttl_map.go └── utils.go /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test-library: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | go-version: [ '1.20', '1.21', '1.22' ] 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Setup Go 16 | uses: actions/setup-go@v4 17 | with: 18 | go-version: ${{ matrix.go-version }} 19 | 20 | - name: Install dependencies 21 | run: go get . 22 | 23 | - name: Go formatting analysis 24 | run: | 25 | if [ -n "$(gofmt -l .)" ]; then 26 | gofmt -d . 27 | exit 1 28 | fi 29 | 30 | - name: Go code quality analysis 31 | run: go vet ./... 32 | 33 | - name: Go unit testing 34 | run: | 35 | go test -race $(go list ./... | grep -v /vendor/) -v -coverprofile=coverage.out 36 | go tool cover -func=coverage.out 37 | 38 | - name: Upload coverage results 39 | uses: actions/upload-artifact@v3 40 | with: 41 | name: coverage 42 | path: coverage.out 43 | 44 | test-lint: 45 | runs-on: ubuntu-latest 46 | strategy: 47 | matrix: 48 | go-version: [ '1.20', '1.21', '1.22' ] 49 | 50 | steps: 51 | - uses: actions/checkout@v4 52 | 53 | - name: Setup Go 54 | uses: actions/setup-go@v4 55 | with: 56 | go-version: ${{ matrix.go-version }} 57 | 58 | - name: Install golangci-lint 59 | run: | 60 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s v1.57.2 61 | 62 | - name: Run golangci-lint 63 | run: ./bin/golangci-lint run -v 64 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # This code is licensed under the terms of the MIT license https://opensource.org/license/mit 2 | # Copyright (c) 2021 Marat Reymers 3 | 4 | ## Golden config for golangci-lint v1.57.2 5 | # 6 | # This is the best config for golangci-lint based on my experience and opinion. 7 | # It is very strict, but not extremely strict. 8 | # Feel free to adapt and change it for your needs. 9 | 10 | run: 11 | # Timeout for analysis, e.g. 30s, 5m. 12 | # Default: 1m 13 | timeout: 3m 14 | 15 | 16 | # This file contains only configs which differ from defaults. 17 | # All possible options can be found here https://github.com/golangci/golangci-lint/blob/master/.golangci.reference.yml 18 | linters-settings: 19 | cyclop: 20 | # The maximal code complexity to report. 21 | # Default: 10 22 | max-complexity: 30 23 | # The maximal average package complexity. 24 | # If it's higher than 0.0 (float) the check is enabled 25 | # Default: 0.0 26 | package-average: 10.0 27 | 28 | errcheck: 29 | # Report about not checking of errors in type assertions: `a := b.(MyStruct)`. 30 | # Such cases aren't reported by default. 31 | # Default: false 32 | check-type-assertions: true 33 | 34 | exhaustive: 35 | # Program elements to check for exhaustiveness. 36 | # Default: [ switch ] 37 | check: 38 | - switch 39 | - map 40 | 41 | exhaustruct: 42 | # List of regular expressions to exclude struct packages and their names from checks. 43 | # Regular expressions must match complete canonical struct package/name/structname. 44 | # Default: [] 45 | exclude: 46 | # std libs 47 | - "^net/http.Client$" 48 | - "^net/http.Cookie$" 49 | - "^net/http.Request$" 50 | - "^net/http.Response$" 51 | - "^net/http.Server$" 52 | - "^net/http.Transport$" 53 | - "^net/url.URL$" 54 | - "^os/exec.Cmd$" 55 | - "^reflect.StructField$" 56 | # public libs 57 | - "^github.com/Shopify/sarama.Config$" 58 | - "^github.com/Shopify/sarama.ProducerMessage$" 59 | - "^github.com/mitchellh/mapstructure.DecoderConfig$" 60 | - "^github.com/prometheus/client_golang/.+Opts$" 61 | - "^github.com/spf13/cobra.Command$" 62 | - "^github.com/spf13/cobra.CompletionOptions$" 63 | - "^github.com/stretchr/testify/mock.Mock$" 64 | - "^github.com/testcontainers/testcontainers-go.+Request$" 65 | - "^github.com/testcontainers/testcontainers-go.FromDockerfile$" 66 | - "^golang.org/x/tools/go/analysis.Analyzer$" 67 | - "^google.golang.org/protobuf/.+Options$" 68 | - "^gopkg.in/yaml.v3.Node$" 69 | 70 | funlen: 71 | # Checks the number of lines in a function. 72 | # If lower than 0, disable the check. 73 | # Default: 60 74 | lines: 120 75 | # Checks the number of statements in a function. 76 | # If lower than 0, disable the check. 77 | # Default: 40 78 | statements: 50 79 | # Ignore comments when counting lines. 80 | # Default false 81 | ignore-comments: true 82 | 83 | gocognit: 84 | # Minimal code complexity to report. 85 | # Default: 30 (but we recommend 10-20) 86 | min-complexity: 20 87 | 88 | gocritic: 89 | # Settings passed to gocritic. 90 | # The settings key is the name of a supported gocritic checker. 91 | # The list of supported checkers can be find in https://go-critic.github.io/overview. 92 | settings: 93 | captLocal: 94 | # Whether to restrict checker to params only. 95 | # Default: true 96 | paramsOnly: false 97 | underef: 98 | # Whether to skip (*x).method() calls where x is a pointer receiver. 99 | # Default: true 100 | skipRecvDeref: false 101 | 102 | gomnd: 103 | # List of function patterns to exclude from analysis. 104 | # Values always ignored: `time.Date`, 105 | # `strconv.FormatInt`, `strconv.FormatUint`, `strconv.FormatFloat`, 106 | # `strconv.ParseInt`, `strconv.ParseUint`, `strconv.ParseFloat`. 107 | # Default: [] 108 | ignored-functions: 109 | - flag.Arg 110 | - flag.Duration.* 111 | - flag.Float.* 112 | - flag.Int.* 113 | - flag.Uint.* 114 | - os.Chmod 115 | - os.Mkdir.* 116 | - os.OpenFile 117 | - os.WriteFile 118 | - prometheus.ExponentialBuckets.* 119 | - prometheus.LinearBuckets 120 | 121 | gomodguard: 122 | blocked: 123 | # List of blocked modules. 124 | # Default: [] 125 | modules: 126 | - github.com/golang/protobuf: 127 | recommendations: 128 | - google.golang.org/protobuf 129 | reason: "see https://developers.google.com/protocol-buffers/docs/reference/go/faq#modules" 130 | - github.com/satori/go.uuid: 131 | recommendations: 132 | - github.com/google/uuid 133 | reason: "satori's package is not maintained" 134 | - github.com/gofrs/uuid: 135 | recommendations: 136 | - github.com/gofrs/uuid/v5 137 | reason: "gofrs' package was not go module before v5" 138 | 139 | gosec: 140 | excludes: 141 | - G404 # This error is triggered when we use math/rand instead of crypto/rand (Too strict) 142 | 143 | govet: 144 | # Enable all analyzers. 145 | # Default: false 146 | enable-all: true 147 | # Disable analyzers by name. 148 | # Run `go tool vet help` to see all analyzers. 149 | # Default: [] 150 | disable: 151 | - fieldalignment # too strict 152 | # Settings per analyzer. 153 | settings: 154 | shadow: 155 | # Whether to be strict about shadowing; can be noisy. 156 | # Default: false 157 | strict: true 158 | 159 | inamedparam: 160 | # Skips check for interface methods with only a single parameter. 161 | # Default: false 162 | skip-single-param: true 163 | 164 | lll: 165 | # Make an issue if line has more characters than this setting. 166 | # Default: 120 167 | line-length: 160 168 | tab-width: 1 169 | 170 | nakedret: 171 | # Make an issue if func has more lines of code than this setting, and it has naked returns. 172 | # Default: 30 173 | max-func-lines: 0 174 | 175 | nolintlint: 176 | # Exclude following linters from requiring an explanation. 177 | # Default: [] 178 | allow-no-explanation: [ funlen, gocognit, lll ] 179 | # Enable to require an explanation of nonzero length after each nolint directive. 180 | # Default: false 181 | require-explanation: true 182 | # Enable to require nolint directives to mention the specific linter being suppressed. 183 | # Default: false 184 | require-specific: true 185 | 186 | perfsprint: 187 | # Optimizes into strings concatenation. 188 | # Default: true 189 | strconcat: false 190 | 191 | rowserrcheck: 192 | # database/sql is always checked 193 | # Default: [] 194 | packages: 195 | - github.com/jmoiron/sqlx 196 | 197 | tenv: 198 | # The option `all` will run against whole test files (`_test.go`) regardless of method/function signatures. 199 | # Otherwise, only methods that take `*testing.T`, `*testing.B`, and `testing.TB` as arguments are checked. 200 | # Default: false 201 | all: true 202 | 203 | 204 | linters: 205 | disable-all: true 206 | enable: 207 | ## enabled by default 208 | - errcheck # checking for unchecked errors, these unchecked errors can be critical bugs in some cases 209 | - gosimple # specializes in simplifying a code 210 | - govet # reports suspicious constructs, such as Printf calls whose arguments do not align with the format string 211 | - ineffassign # detects when assignments to existing variables are not used 212 | - staticcheck # is a go vet on steroids, applying a ton of static analysis checks 213 | - typecheck # like the front-end of a Go compiler, parses and type-checks Go code 214 | - unused # checks for unused constants, variables, functions and types 215 | ## disabled by default 216 | - asasalint # checks for pass []any as any in variadic func(...any) 217 | - asciicheck # checks that your code does not contain non-ASCII identifiers 218 | - bidichk # checks for dangerous unicode character sequences 219 | - bodyclose # checks whether HTTP response body is closed successfully 220 | - cyclop # checks function and package cyclomatic complexity 221 | - dupl # tool for code clone detection 222 | - durationcheck # checks for two durations multiplied together 223 | - errname # checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error 224 | - errorlint # finds code that will cause problems with the error wrapping scheme introduced in Go 1.13 225 | - execinquery # checks query string in Query function which reads your Go src files and warning it finds 226 | - exhaustive # checks exhaustiveness of enum switch statements 227 | - exportloopref # checks for pointers to enclosing loop variables 228 | - forbidigo # forbids identifiers 229 | - funlen # tool for detection of long functions 230 | - gocheckcompilerdirectives # validates go compiler directive comments (//go:) 231 | - gochecknoinits # checks that no init functions are present in Go code 232 | - gochecksumtype # checks exhaustiveness on Go "sum types" 233 | - gocognit # computes and checks the cognitive complexity of functions 234 | - goconst # finds repeated strings that could be replaced by a constant 235 | - gocritic # provides diagnostics that check for bugs, performance and style issues 236 | - gocyclo # computes and checks the cyclomatic complexity of functions 237 | - godot # checks if comments end in a period 238 | - goimports # in addition to fixing imports, goimports also formats your code in the same style as gofmt 239 | - gomnd # detects magic numbers 240 | - gomoddirectives # manages the use of 'replace', 'retract', and 'excludes' directives in go.mod 241 | - gomodguard # allow and block lists linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations 242 | - goprintffuncname # checks that printf-like functions are named with f at the end 243 | - gosec # inspects source code for security problems 244 | - lll # reports long lines 245 | - loggercheck # checks key value pairs for common logger libraries (kitlog,klog,logr,zap) 246 | - makezero # finds slice declarations with non-zero initial length 247 | - mirror # reports wrong mirror patterns of bytes/strings usage 248 | - musttag # enforces field tags in (un)marshaled structs 249 | - nakedret # finds naked returns in functions greater than a specified function length 250 | - nestif # reports deeply nested if statements 251 | - nilerr # finds the code that returns nil even if it checks that the error is not nil 252 | - nilnil # checks that there is no simultaneous return of nil error and an invalid value 253 | - noctx # finds sending http request without context.Context 254 | - nolintlint # reports ill-formed or insufficient nolint directives 255 | - nonamedreturns # reports all named returns 256 | - perfsprint # checks that fmt.Sprintf can be replaced with a faster alternative 257 | - prealloc # [premature optimization, but can be used in some cases] finds slice declarations that could potentially be preallocated 258 | - predeclared # finds code that shadows one of Go's predeclared identifiers 259 | - promlinter # checks Prometheus metrics naming via promlint 260 | - protogetter # reports direct reads from proto message fields when getters should be used 261 | - reassign # checks that package variables are not reassigned 262 | - revive # fast, configurable, extensible, flexible, and beautiful linter for Go, drop-in replacement of golint 263 | - rowserrcheck # checks whether Err of rows is checked successfully 264 | - sloglint # ensure consistent code style when using log/slog 265 | - spancheck # checks for mistakes with OpenTelemetry/Census spans 266 | - sqlclosecheck # checks that sql.Rows and sql.Stmt are closed 267 | - stylecheck # is a replacement for golint 268 | - tagalign # checks that struct tags are well aligned 269 | - tenv # detects using os.Setenv instead of t.Setenv since Go1.17 270 | - testableexamples # checks if examples are testable (have an expected output) 271 | - testifylint # checks usage of github.com/stretchr/testify 272 | - testpackage # makes you use a separate _test package 273 | - tparallel # detects inappropriate usage of t.Parallel() method in your Go test codes 274 | - unconvert # removes unnecessary type conversions 275 | - unparam # reports unused function parameters 276 | - usestdlibvars # detects the possibility to use variables/constants from the Go standard library 277 | - wastedassign # finds wasted assignment statements 278 | - whitespace # detects leading and trailing whitespace 279 | 280 | ## you may want to enable 281 | #- decorder # checks declaration order and count of types, constants, variables and functions 282 | #- exhaustruct # [highly recommend to enable] checks if all structure fields are initialized 283 | #- gci # controls golang package import order and makes it always deterministic 284 | #- ginkgolinter # [if you use ginkgo/gomega] enforces standards of using ginkgo and gomega 285 | #- godox # detects FIXME, TODO and other comment keywords 286 | #- gochecknoglobals # checks that no global variables exist 287 | #- goheader # checks is file header matches to pattern 288 | #- inamedparam # [great idea, but too strict, need to ignore a lot of cases by default] reports interfaces with unnamed method parameters 289 | #- interfacebloat # checks the number of methods inside an interface 290 | #- intrange # finds places where for loops could make use of an integer range 291 | #- ireturn # accept interfaces, return concrete types 292 | #- varnamelen # [great idea, but too many false positives] checks that the length of a variable's name matches its scope 293 | #- wrapcheck # checks that errors returned from external packages are wrapped 294 | #- zerologlint # detects the wrong usage of zerolog that a user forgets to dispatch zerolog.Event 295 | 296 | ## disabled 297 | #- containedctx # detects struct contained context.Context field 298 | #- contextcheck # [too many false positives] checks the function whether use a non-inherited context 299 | #- copyloopvar # detects places where loop variables are copied 300 | #- depguard # [replaced by gomodguard] checks if package imports are in a list of acceptable packages 301 | #- dogsled # checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) 302 | #- dupword # [useless without config] checks for duplicate words in the source code 303 | #- errchkjson # [don't see profit + I'm against of omitting errors like in the first example https://github.com/breml/errchkjson] checks types passed to the json encoding functions. Reports unsupported types and optionally reports occasions, where the check for the returned error can be omitted 304 | #- forcetypeassert # [replaced by errcheck] finds forced type assertions 305 | #- goerr113 # [too strict] checks the errors handling expressions 306 | #- gofmt # [replaced by goimports] checks whether code was gofmt-ed 307 | #- gofumpt # [replaced by goimports, gofumports is not available yet] checks whether code was gofumpt-ed 308 | #- gosmopolitan # reports certain i18n/l10n anti-patterns in your Go codebase 309 | #- grouper # analyzes expression groups 310 | #- importas # enforces consistent import aliases 311 | #- maintidx # measures the maintainability index of each function 312 | #- misspell # [useless] finds commonly misspelled English words in comments 313 | #- nlreturn # [too strict and mostly code is not more readable] checks for a new line before return and branch statements to increase code clarity 314 | #- nosprintfhostport # checks for misuse of Sprintf to construct a host with port in a URL 315 | #- paralleltest # [too many false positives] detects missing usage of t.Parallel() method in your Go test 316 | #- tagliatelle # checks the struct tags 317 | #- thelper # detects golang test helpers without t.Helper() call and checks the consistency of test helpers 318 | #- wsl # [too strict and mostly code is not more readable] whitespace linter forces you to use empty lines 319 | 320 | 321 | issues: 322 | # Maximum count of issues with the same text. 323 | # Set to 0 to disable. 324 | # Default: 3 325 | max-same-issues: 50 326 | 327 | exclude-rules: 328 | - source: "(noinspection|TODO)" 329 | linters: [ godot ] 330 | - source: "//noinspection" 331 | linters: [ gocritic ] 332 | - path: "_test\\.go" 333 | linters: 334 | - bodyclose 335 | - dupl 336 | - funlen 337 | - goconst 338 | - gosec 339 | - noctx 340 | - wrapcheck 341 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.1.2 2 | 3 | - Allow sending messages with a TTL via `PublishWithOptions` by adding a new `TTL` property in `PublishingOptions` ([PR](https://github.com/KardinalAI/gorabbit/pull/19)). 4 | 5 | # 1.1.1 6 | 7 | - Minor fix for correct usage of the `ConnectionName` parameter, and the possibility to declare it via environment variables ([PR](https://github.com/KardinalAI/gorabbit/pull/18)). 8 | 9 | # 1.1.0 10 | 11 | - Allow setting a connection name ([PR](https://github.com/KardinalAI/gorabbit/pull/8)) 12 | - Allow configuring a custom marshaller ([PR](https://github.com/KardinalAI/gorabbit/pull/10)) 13 | - Allow setting autoDelete on queues ([PR](https://github.com/KardinalAI/gorabbit/pull/11)) 14 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official email address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | [INSERT CONTACT METHOD]. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. 120 | 121 | Community Impact Guidelines were inspired by 122 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 123 | 124 | For answers to common questions about this code of conduct, see the FAQ at 125 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available 126 | at [https://www.contributor-covenant.org/translations][translations]. 127 | 128 | [homepage]: https://www.contributor-covenant.org 129 | 130 | [v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html 131 | 132 | [Mozilla CoC]: https://github.com/mozilla/diversity 133 | 134 | [FAQ]: https://www.contributor-covenant.org/faq 135 | 136 | [translations]: https://www.contributor-covenant.org/translations 137 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Gorabbit 2 | 3 | Thank you for your interest in contributing to Gorabbit! We welcome contributions from the community to make our project 4 | better. 5 | 6 | ## Getting Started 7 | 8 | Before you start contributing, please make sure you have read the [README](./README.md) to understand the project and 9 | its goals. 10 | 11 | ## How Can I Contribute? 12 | 13 | ### Reporting Bugs 14 | 15 | If you come across a bug or unexpected behavior, please help us by submitting a detailed bug report. Use 16 | the [GitHub Issues](https://github.com/KardinalAI/gorabbit/issues) to report bugs, and make sure to include the 17 | following information: 18 | 19 | - A clear and descriptive title. 20 | - A detailed description of the issue, including steps to reproduce. 21 | - Information about your environment: Go version, operating system, etc. 22 | 23 | ### Suggesting Enhancements 24 | 25 | If you have ideas for enhancements or new features, we would love to hear them! Create an issue 26 | on [GitHub](https://github.com/KardinalAI/gorabbit/issues) with a clear description of your proposal, and we can discuss 27 | it together. 28 | 29 | ### Pull Requests 30 | 31 | We welcome contributions in the form of pull requests. If you want to contribute code, please follow these guidelines: 32 | 33 | 1. Fork the repository and create your branch from `v1`. 34 | 2. Make sure your code adheres to the [Go coding standards](https://golang.org/doc/effective_go). 35 | 3. Make sure your code respects all [linting rules](./.golangci.yml) using [golangci-lint](https://golangci-lint.run/) 36 | version 1.57.2. 37 | 4. Write clear commit messages and include documentation if necessary. 38 | 5. Make sure your code passes the existing tests. 39 | 6. Open a pull request, providing a clear description of your changes. 40 | 41 | ## Code of Conduct 42 | 43 | Please note that this project has a [Code of Conduct](./CODE_OF_CONDUCT.md). By participating in this project, you are 44 | expected to uphold this code. Please report any unacceptable behavior to an administrator. 45 | 46 | ## License 47 | 48 | By contributing to Gorabbit, you agree that your contributions will be licensed under the [LICENSE](./LICENSE) file 49 | associated with this project. 50 | 51 | Thank you for your contributions! 52 | 53 | KardinalAI -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 KardinalAI 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gorabbit 2 | 3 |

4 | logo 5 |

6 | 7 | Gorabbit is a wrapper that provides high level and robust RabbitMQ operations through a client or a manager. 8 | 9 | This wrapper depends on the official [Go RabbitMQ plugin](https://github.com/rabbitmq/amqp091-go). 10 | 11 | * [Installation](#installation) 12 | * [Go Module](#go-module) 13 | * [Environment Variables](#environment-variables) 14 | * [Always On Mechanism](#always-on-mechanism) 15 | * [Client](#client) 16 | * [Initialization](#client-initialization) 17 | * [Options](#client-options) 18 | * [Default Options](#client-with-default-options) 19 | * [Custom Options](#client-with-custom-options) 20 | * [Builder](#client-options-using-the-builder) 21 | * [Struct](#client-options-using-struct-initialization) 22 | * [Disconnection](#client-disconnection) 23 | * [Publishing](#publishing) 24 | * [Consuming](#consuming) 25 | * [Ready and Health Checks](#ready-and-health-checks) 26 | * [Manager](#manager) 27 | * [Initialization](#manager-initialization) 28 | * [Options](#manager-options) 29 | * [Default Options](#manager-with-default-options) 30 | * [Custom Options](#manager-with-custom-options) 31 | * [Builder](#manager-options-using-the-builder) 32 | * [Struct](#manager-options-using-struct-initialization) 33 | * [Disconnection](#manager-disconnection) 34 | * [Operations](#manager-operations) 35 | * [Exchange Creation](#exchange-creation) 36 | * [Queue Creation](#queue-creation) 37 | * [Binding Creation](#binding-creation) 38 | * [Message Count](#queue-messages-count) 39 | * [Push Message](#push-message) 40 | * [Pop Message](#pop-message) 41 | * [Purge Queue](#purge-queue) 42 | * [Delete Queue](#delete-queue) 43 | * [Delete Exchange](#delete-exchange) 44 | * [Setup From Definitions](#setup-from-schema-definition-file) 45 | 46 | ## Installation 47 | 48 | ### Go module 49 | 50 | ```bash 51 | go get github.com/KardinalAI/gorabbit 52 | ``` 53 | 54 | ### Environment variables 55 | 56 | The client's and manager's `Mode` can also be set via an environment variable that will **override** the manually 57 | entered value. 58 | 59 | ```dotenv 60 | GORABBIT_MODE: debug # possible values: release or debug 61 | ``` 62 | 63 | The client and manager can also be completely disabled via the following environment variable: 64 | 65 | ```dotenv 66 | GORABBIT_DISABLED: true # possible values: true, false, 1, or 0 67 | ``` 68 | 69 | ## Always-on mechanism 70 | 71 | Here is a visual representation of the always-on mechanism of a connection and channel when the `KeepAlive` flag is set 72 | to true. 73 | 74 | ![Always on mechanism](assets/always-on-mechanism.png) 75 | 76 | ## Client 77 | 78 | The gorabbit client offers 2 main functionalities: 79 | 80 | * Publishing 81 | * Consuming 82 | 83 | Additionally, the client also provides a ready check and a health check. 84 | 85 | ### Client initialization 86 | 87 | A client can be initialized via the constructor `NewClient`. This constructor takes `ClientOptions` as an optional 88 | parameter. 89 | 90 | ### Client options 91 | 92 | | Property | Description | Default Value | 93 | |---------------------|---------------------------------------------------------------|---------------| 94 | | Host | The hostname of the RabbitMQ server | 127.0.0.1 | 95 | | Port | The port of the RabbitMQ server | 5672 | 96 | | Username | The plain authentication username | guest | 97 | | Password | The plain authentication password | guest | 98 | | Vhost | The specific vhost to use when connection to CloudAMQP | | 99 | | UseTLS | The flag that activates the use of TLS (amqps) | false | 100 | | ConnectionName | The desired connection name | Gorabbit | 101 | | KeepAlive | The flag that activates retry and re-connect mechanisms | true | 102 | | RetryDelay | The delay between each retry and re-connection | 3 seconds | 103 | | MaxRetry | The max number of message retry if it failed to process | 5 | 104 | | PublishingCacheTTL | The time to live for a failed publish when set in cache | 60 seconds | 105 | | PublishingCacheSize | The max number of failed publish to add into cache | 128 | 106 | | Mode | The mode defines whether logs are shown or not | Release | 107 | | Marshaller | The content type used for messages and how they're marshalled | JSON | 108 | 109 | ### Client with default options 110 | 111 | Passing `nil` options will trigger the client to use default values (host, port, credentials, etc...) 112 | via `DefaultClientOptions()`. 113 | 114 | ```go 115 | client := gorabbit.NewClient(nil) 116 | ``` 117 | 118 | You can also explicitly pass `DefaultClientOptions()` for a cleaner initialization. 119 | 120 | ```go 121 | client := gorabbit.NewClient(gorabbit.DefaultClientOptions()) 122 | ``` 123 | 124 | Finally, passing a `NewClientOptions()` method also initializes default values if not overwritten. 125 | 126 | ```go 127 | client := gorabbit.NewClient(gorabbit.NewClientOptions()) 128 | ``` 129 | 130 | ### Client with options from environment variables 131 | 132 | You can instantiate a client from environment variables, without the need of manually specifying options in the code. 133 | 134 | ```go 135 | client := gorabbit.NewClientFromEnv() 136 | ``` 137 | 138 | Here are the following supported environment variables: 139 | 140 | * `RABBITMQ_HOST`: Defines the host, 141 | * `RABBITMQ_PORT`: Defines the port, 142 | * `RABBITMQ_USERNAME`: Defines the username, 143 | * `RABBITMQ_PASSWORD`: Defines the password, 144 | * `RABBITMQ_VHOST`: Defines the vhost, 145 | * `RABBITMQ_USE_TLS`: Defines whether to use TLS or no. 146 | * `RABBITMQ_CONNECTION_NAME`: Defines the desired connection name. 147 | 148 | **Note that environment variables are all optional, so missing keys will be replaced by their corresponding default.** 149 | 150 | ### Client with custom options 151 | 152 | We can input custom values for a specific property, either via the built-in builder or via direct struct initialization. 153 | 154 | #### Client options using the builder 155 | 156 | `NewClientOptions()` and `DefaultClientOptions()` both return an instance of `*ClientOptions` that can act as a builder. 157 | 158 | ```go 159 | options := gorabbit.NewClientOptions(). 160 | SetMode(gorabbit.Debug). 161 | SetCredentials("root", "password"). 162 | SetRetryDelay(5 * time.Second) 163 | 164 | client := gorabbit.NewClient(options) 165 | ``` 166 | 167 | > :information_source: There is a setter method for each property. 168 | 169 | #### Client options using struct initialization 170 | 171 | `ClientOptions` is an exported type, so it can be used directly. 172 | 173 | ```go 174 | options := gorabbit.ClientOptions { 175 | Host: "localhost", 176 | Port: 5673, 177 | Username: "root", 178 | Password: "password", 179 | ... 180 | } 181 | 182 | client := gorabbit.NewClient(&options) 183 | ``` 184 | 185 | > :warning: Direct initialization via the struct **does not use default values on missing properties**, so be sure to 186 | > fill in every property available. 187 | 188 | ### Client disconnection 189 | 190 | When a client is initialized, to prevent a leak, always disconnect it when no longer needed. 191 | 192 | ```go 193 | client := gorabbit.NewClient(gorabbit.DefaultClientOptions()) 194 | defer client.Disconnect() 195 | ``` 196 | 197 | ### Publishing 198 | 199 | To send a message, the client offers two simple methods: `Publish` and `PublishWithOptions`. The required arguments for 200 | publishing are: 201 | 202 | * Exchange (which exchange the message should be sent to) 203 | * Routing Key 204 | * Payload (`interface{}`, the object will be marshalled internally) 205 | 206 | Example of sending a simple string 207 | 208 | ```go 209 | err := client.Publish("events_exchange", "event.foo.bar.created", "foo string") 210 | ``` 211 | 212 | Example of sending an object 213 | 214 | ```go 215 | type foo struct { 216 | Action string 217 | } 218 | 219 | err := client.Publish("events_exchange", "event.foo.bar.created", foo{Action: "bar"}) 220 | ``` 221 | 222 | Optionally, you can set the message's `Priority`, `DeliveryMode` and `Expiration` via the `PublishWithOptions` method. 223 | 224 | ```go 225 | options := gorabbit.SendOptions(). 226 | SetPriority(gorabbit.PriorityMedium). 227 | SetDeliveryMode(gorabbit.Persistent). 228 | SetTTL(5*time.Second) 229 | 230 | err := client.PublishWithOptions("events_exchange", "event.foo.bar.created", "foo string", options) 231 | ``` 232 | 233 | > :information_source: If the `KeepAlive` flag is set to true when initializing the client, failed publishing will be 234 | > cached once 235 | > and re-published as soon as the channel is back up. 236 | > 237 | > ![publishing safeguard](assets/publishing-safeguard.png) 238 | 239 | ### Consuming 240 | 241 | To consume messages, gorabbit offers a very simple asynchronous consumer method `Consume` that takes a `MessageConsumer` 242 | as argument. Error handling, acknowledgement, negative acknowledgement and rejection are all done internally by the 243 | consumer. 244 | 245 | ```go 246 | err := client.RegisterConsumer(gorabbit.MessageConsumer{ 247 | Queue: "events_queue", 248 | Name: "toto_consumer", 249 | PrefetchSize: 0, 250 | PrefetchCount: 10, 251 | AutoAck: false, 252 | ConcurrentProcess: false, 253 | Handlers: gorabbit.MQTTMessageHandlers{ 254 | "event.foo.bar.created": func (payload []byte) error { 255 | fmt.Println(string(payload)) 256 | 257 | return nil 258 | }, 259 | }, 260 | }) 261 | ``` 262 | 263 | * Queue: The queue to consume messages from 264 | * Name: Unique identifier for the consumer 265 | * PrefetchSize: The maximum size of messages that can be processed at the same time 266 | * PrefetchCount: The maximum number of messages that can be processed at the same time 267 | * AutoAck: Automatic acknowledgement of messages upon reception 268 | * ConcurrentProcess: Asynchronous handling of deliveries 269 | * Handlers: A list of handlers for specified routes 270 | 271 | **NB:** [RabbitMQ Wildcards](https://www.cloudamqp.com/blog/rabbitmq-topic-exchange-explained.html) are also supported. 272 | If multiple routing keys have the same handler, a wildcard can be used, for example: 273 | `event.foo.bar.*` or `event.foo.#`. 274 | 275 | > :information_source: If the `KeepAlive` flag is set to true when initializing the client, consumers will 276 | > auto-reconnect after a connection loss. 277 | > This mechanism is indefinite and therefore, consuming from a non-existent queue will trigger an error repeatedly but 278 | > will not affect 279 | > other consumptions. This is because each consumer has its **own channel**. 280 | > 281 | > ![consumer safeguard](assets/consumer-safeguard.png) 282 | 283 | ### Ready and Health checks 284 | 285 | The client offers `IsReady()` and `IsHealthy()` checks that can be used for monitoring. 286 | 287 | **Ready:** Verifies that connections are opened and ready to launch new operations. 288 | 289 | **Healthy:** Verifies that both connections and channels are opened, ready and ongoing operations are working 290 | (Consumers are consuming). 291 | 292 | ## Manager 293 | 294 | The gorabbit manager offers multiple management operations: 295 | 296 | * Exchange, queue and bindings creation 297 | * Exchange and queue deletion 298 | * Queue evaluation: Exists, number of messages 299 | * Queue operations: Pop message, push message, purge 300 | 301 | > :warning: A manager should only be used for either testing RabbitMQ functionalities or setting up a RabbitMQ server. 302 | > The manager does not provide robust mechanisms of retry and reconnection like the client. 303 | 304 | ### Manager initialization 305 | 306 | A manager can be initialized via the constructor `NewManager`. This constructor takes `ManagerOptions` as an optional 307 | parameter. 308 | 309 | ### Manager options 310 | 311 | | Property | Description | Default Value | 312 | |------------|---------------------------------------------------------------|---------------| 313 | | Host | The hostname of the RabbitMQ server | 127.0.0.1 | 314 | | Port | The port of the RabbitMQ server | 5672 | 315 | | Username | The plain authentication username | guest | 316 | | Password | The plain authentication password | guest | 317 | | Vhost | The specific vhost to use when connection to CloudAMQP | | 318 | | UseTLS | The flag that activates the use of TLS (amqps) | false | 319 | | Mode | The mode defines whether logs are shown or not | Release | 320 | | Marshaller | The content type used for messages and how they're marshalled | JSON | 321 | 322 | ### Manager with default options 323 | 324 | Passing `nil` options will trigger the manager to use default values (host, port, credentials, etc...) 325 | via `DefaultManagerOptions()`. 326 | 327 | ```go 328 | manager := gorabbit.NewManager(nil) 329 | ``` 330 | 331 | You can also explicitly pass `DefaultManagerOptions()` for a cleaner initialization. 332 | 333 | ```go 334 | manager := gorabbit.NewManager(gorabbit.DefaultManagerOptions()) 335 | ``` 336 | 337 | Finally, passing a `NewManagerOptions()` method also initializes default values if not overwritten. 338 | 339 | ```go 340 | manager := gorabbit.NewManager(gorabbit.NewManagerOptions()) 341 | ``` 342 | 343 | ### Manager with options from environment variables 344 | 345 | You can instantiate a manager from environment variables, without the need of manually specifying options in the code. 346 | 347 | ```go 348 | manager := gorabbit.NewManagerFromEnv() 349 | ``` 350 | 351 | Here are the following supported environment variables: 352 | 353 | * `RABBITMQ_HOST`: Defines the host, 354 | * `RABBITMQ_PORT`: Defines the port, 355 | * `RABBITMQ_USERNAME`: Defines the username, 356 | * `RABBITMQ_PASSWORD`: Defines the password, 357 | * `RABBITMQ_VHOST`: Defines the vhost, 358 | * `RABBITMQ_USE_TLS`: Defines whether to use TLS or no. 359 | 360 | **Note that environment variables are all optional, so missing keys will be replaced by their corresponding default.** 361 | 362 | ### Manager with custom options 363 | 364 | We can input custom values for a specific property, either via the built-in builder or via direct struct initialization. 365 | 366 | #### Manager options using the builder 367 | 368 | `NewManagerOptions()` and `DefaultManagerOptions()` both return an instance of `*ManagerOptions` that can act as a 369 | builder. 370 | 371 | ```go 372 | options := gorabbit.NewManagerOptions(). 373 | SetMode(gorabbit.Debug). 374 | SetCredentials("root", "password") 375 | 376 | manager := gorabbit.NewManager(options) 377 | ``` 378 | 379 | > :information_source: There is a setter method for each property. 380 | 381 | #### Manager options using struct initialization 382 | 383 | `ManagerOptions` is an exported type, so it can be used directly. 384 | 385 | ```go 386 | options := gorabbit.ManagerOptions { 387 | Host: "localhost", 388 | Port: 5673, 389 | Username: "root", 390 | Password: "password", 391 | Mode: gorabbit.Debug, 392 | } 393 | 394 | manager := gorabbit.NewManager(&options) 395 | ``` 396 | 397 | > :warning: Direct initialization via the struct **does not use default values on missing properties**, so be sure to 398 | > fill in every property available. 399 | 400 | ### Manager disconnection 401 | 402 | When a manager is initialized, to prevent a leak, always disconnect it when no longer needed. 403 | 404 | ```go 405 | manager := gorabbit.NewManager(gorabbit.DefaultManagerOptions()) 406 | defer manager.Disconnect() 407 | ``` 408 | 409 | ### Manager operations 410 | 411 | The manager offers all necessary operations to manager a RabbitMQ server. 412 | 413 | #### Exchange creation 414 | 415 | Creates an exchange with optional arguments. 416 | 417 | ```go 418 | err := manager.CreateExchange(gorabbit.ExchangeConfig{ 419 | Name: "events_exchange", 420 | Type: gorabbit.ExchangeTypeTopic, 421 | Persisted: false, 422 | Args: nil, 423 | }) 424 | ``` 425 | 426 | #### Queue creation 427 | 428 | Creates a queue with optional arguments and bindings if declared. 429 | 430 | ```go 431 | err := manager.CreateQueue(gorabbit.QueueConfig{ 432 | Name: "events_queue", 433 | Durable: false, 434 | Exclusive: false, 435 | Args: nil, 436 | Bindings: &[]gorabbit.BindingConfig{ 437 | { 438 | RoutingKey: "event.foo.bar.created", 439 | Exchange: "events_exchange", 440 | }, 441 | }, 442 | }) 443 | ``` 444 | 445 | #### Binding creation 446 | 447 | Binds a queue to an exchange via a given routing key. 448 | 449 | ```go 450 | err := manager.BindExchangeToQueueViaRoutingKey("events_exchange", "events_queue", "event.foo.bar.created") 451 | ``` 452 | 453 | #### Queue messages count 454 | 455 | Returns the number of messages in a queue, or an error if the queue does not exist. This method can also evaluate the 456 | existence of a queue. 457 | 458 | ```go 459 | messageCount, err := manager.GetNumberOfMessages("events_queue") 460 | ``` 461 | 462 | #### Push message 463 | 464 | Pushes a single message to a given exchange. 465 | 466 | ```go 467 | err := manager.PushMessageToExchange("events_exchange", "event.foo.bar.created", "single_message_payload") 468 | ``` 469 | 470 | #### Pop message 471 | 472 | Retrieves a single message from a given queue and auto acknowledges it if `autoAck` is set to true. 473 | 474 | ```go 475 | message, err := manager.PopMessageFromQueue("events_queue", true) 476 | ``` 477 | 478 | #### Purge queue 479 | 480 | Deletes all messages from a given queue. 481 | 482 | ```go 483 | err := manager.PurgeQueue("events_queue") 484 | ``` 485 | 486 | #### Delete queue 487 | 488 | Deletes a given queue. 489 | 490 | ```go 491 | err := manager.DeleteQueue("events_queue") 492 | ``` 493 | 494 | #### Delete exchange 495 | 496 | Deletes a given exchange. 497 | 498 | ```go 499 | err := manager.DeleteExchange("events_exchange") 500 | ``` 501 | 502 | #### Setup from schema definition file 503 | 504 | You can setup exchanges, queues and bindings automatically by referencing a 505 | [RabbitMQ Schema Definition](assets/definitions.example.json) JSON file. 506 | 507 | ```go 508 | err := manager.SetupFromDefinitions("/path/to/definitions.json") 509 | ``` 510 | 511 | > :warning: The standard RabbitMQ definitions file contains configurations for 512 | > `users`, `vhosts` and `permissions`. Those configurations are not taken into consideration 513 | > in the `SetupFromDefinitions` method. 514 | 515 | ## Launch Local RabbitMQ Server 516 | 517 | To run a local rabbitMQ server quickly with a docker container, simply run the following command: 518 | 519 | ```bash 520 | docker run -it --rm --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:3-management 521 | ``` 522 | 523 | It will launch a local RabbitMQ server mapped on port 5672, and the management dashboard will be mapped on 524 | port 15672 accessible on localhost:15672 with a username "guest" and password "guest". 525 | 526 | ## License 527 | **Gorabbit** is licensed under the [MIT](LICENSE). -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | Must have or Nice to have features. 4 | 5 | ## Must Haves 6 | 7 | - [ ] Unregister consumer via a `UnregisterConsumer` 8 | - [x] ~~Send messages with a TTL~~ 9 | - [ ] Send messages with a definable header 10 | 11 | ## Nice to have 12 | 13 | - [ ] Example projects for consumers and producers 14 | 15 | ## To Revisit/Rethink 16 | 17 | - [ ] Review the `publishingCache` in `channel.go` 18 | - [ ] Review the logger 19 | - [ ] Review the linter and redefine some rules if they are too strict 20 | 21 | ## To Fix 22 | 23 | - [ ] Concurrent consumption throwing errors when `PrefetchSize` is set 24 | - [ ] Consumer wildcard validator does not match all possibilities -------------------------------------------------------------------------------- /assets/always-on-mechanism.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KardinalAI/gorabbit/9796c574c1eedefd10ba1ca988c3096f387f3e79/assets/always-on-mechanism.png -------------------------------------------------------------------------------- /assets/consumer-safeguard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KardinalAI/gorabbit/9796c574c1eedefd10ba1ca988c3096f387f3e79/assets/consumer-safeguard.png -------------------------------------------------------------------------------- /assets/definitions.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "exchanges": [ 3 | { 4 | "name": "foo_exchange", 5 | "vhost": "/", 6 | "type": "topic", 7 | "durable": true, 8 | "auto_delete": false, 9 | "internal": false, 10 | "arguments": {} 11 | }, 12 | { 13 | "name": "bar_exchange", 14 | "vhost": "/", 15 | "type": "topic", 16 | "durable": true, 17 | "auto_delete": false, 18 | "internal": false, 19 | "arguments": {} 20 | } 21 | ], 22 | "queues": [ 23 | { 24 | "name": "foo", 25 | "vhost": "/", 26 | "durable": true, 27 | "auto_delete": false, 28 | "arguments": {} 29 | }, 30 | { 31 | "name": "bar", 32 | "vhost": "/", 33 | "durable": true, 34 | "auto_delete": false, 35 | "arguments": {} 36 | } 37 | ], 38 | "bindings": [ 39 | { 40 | "source": "foo_exchange", 41 | "vhost": "/", 42 | "destination": "foo", 43 | "destination_type": "queue", 44 | "routing_key": "event.foo.#", 45 | "arguments": {} 46 | }, 47 | { 48 | "source": "bar_exchange", 49 | "vhost": "/", 50 | "destination": "bar", 51 | "destination_type": "queue", 52 | "routing_key": "event.bar.#", 53 | "arguments": {} 54 | } 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /assets/gorabbit-logo-lg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KardinalAI/gorabbit/9796c574c1eedefd10ba1ca988c3096f387f3e79/assets/gorabbit-logo-lg.jpg -------------------------------------------------------------------------------- /assets/gorabbit-logo-md.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KardinalAI/gorabbit/9796c574c1eedefd10ba1ca988c3096f387f3e79/assets/gorabbit-logo-md.jpg -------------------------------------------------------------------------------- /assets/gorabbit-logo-sm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KardinalAI/gorabbit/9796c574c1eedefd10ba1ca988c3096f387f3e79/assets/gorabbit-logo-sm.jpg -------------------------------------------------------------------------------- /assets/publishing-safeguard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KardinalAI/gorabbit/9796c574c1eedefd10ba1ca988c3096f387f3e79/assets/publishing-safeguard.png -------------------------------------------------------------------------------- /channel.go: -------------------------------------------------------------------------------- 1 | package gorabbit 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/google/uuid" 9 | 10 | amqp "github.com/rabbitmq/amqp091-go" 11 | ) 12 | 13 | // amqpChannels is a simple wrapper of an amqpChannel slice. 14 | type amqpChannels []*amqpChannel 15 | 16 | // publishingChannel loops through all channels and returns the first available publisher channel if it exists. 17 | func (a amqpChannels) publishingChannel() *amqpChannel { 18 | for _, channel := range a { 19 | if channel != nil && channel.connectionType == connectionTypePublisher { 20 | return channel 21 | } 22 | } 23 | 24 | return nil 25 | } 26 | 27 | // updateParentConnection updates every channel's parent connection. 28 | func (a amqpChannels) updateParentConnection(conn *amqp.Connection) { 29 | for _, channel := range a { 30 | channel.connection = conn 31 | } 32 | } 33 | 34 | // amqpChannel holds information about the management of the native amqp.Channel. 35 | type amqpChannel struct { 36 | // ctx is the parent context and acts as a safeguard. 37 | ctx context.Context 38 | 39 | // connection is the native amqp.Connection. 40 | connection *amqp.Connection 41 | 42 | // channel is the native amqp.Channel. 43 | channel *amqp.Channel 44 | 45 | // keepAlive is the flag that will define whether active guards and re-connections are enabled or not. 46 | keepAlive bool 47 | 48 | // retryDelay defines the delay to wait before re-connecting if the channel was closed and the keepAlive flag is set to true. 49 | retryDelay time.Duration 50 | 51 | // consumer is the MessageConsumer that holds all necessary information for the consumption of messages. 52 | consumer *MessageConsumer 53 | 54 | // consumptionCtx holds the consumption context. 55 | consumptionCtx context.Context 56 | 57 | // consumptionCancel is the cancel function of the consumptionCtx. 58 | consumptionCancel context.CancelFunc 59 | 60 | // consumptionHealth manages the status of all active consumptions. 61 | consumptionHealth consumptionHealth 62 | 63 | // publishingCache manages the caching of unpublished messages due to a connection error. 64 | publishingCache *ttlMap[string, mqttPublishing] 65 | 66 | // maxRetry defines the retry header for each message. 67 | maxRetry uint 68 | 69 | // closed is an inner property that switches to true if the channel was explicitly closed. 70 | closed bool 71 | 72 | // logger logs events. 73 | logger logger 74 | 75 | // releaseLogger forces logs not matter the mode. It is used to log important things. 76 | releaseLogger logger 77 | 78 | // connectionType defines the connectionType. 79 | connectionType connectionType 80 | 81 | // marshaller defines the marshalling method used to encode messages. 82 | marshaller Marshaller 83 | } 84 | 85 | // newConsumerChannel instantiates a new consumerChannel and amqpChannel for method inheritance. 86 | // - ctx is the parent context. 87 | // - connection is the parent amqp.Connection. 88 | // - keepAlive will keep the channel alive if true. 89 | // - retryDelay defines the delay between each retry, if the keepAlive flag is set to true. 90 | // - consumer is the MessageConsumer that will hold consumption information. 91 | // - maxRetry is the retry header for each message. 92 | // - logger is the parent logger. 93 | // - marshaller is the Marshaller used for encoding messages. 94 | func newConsumerChannel( 95 | ctx context.Context, 96 | connection *amqp.Connection, 97 | keepAlive bool, 98 | retryDelay time.Duration, 99 | consumer *MessageConsumer, 100 | logger logger, 101 | marshaller Marshaller, 102 | ) *amqpChannel { 103 | channel := &amqpChannel{ 104 | ctx: ctx, 105 | connection: connection, 106 | keepAlive: keepAlive, 107 | retryDelay: retryDelay, 108 | logger: inheritLogger(logger, map[string]interface{}{ 109 | "context": "channel", 110 | "type": connectionTypeConsumer, 111 | "consumer": consumer.Name, 112 | "queue": consumer.Queue, 113 | }), 114 | releaseLogger: &stdLogger{ 115 | logger: newLogrus(), 116 | identifier: libraryName, 117 | logFields: map[string]interface{}{ 118 | "context": "channel", 119 | "type": connectionTypeConsumer, 120 | "consumer": consumer.Name, 121 | "queue": consumer.Queue, 122 | }, 123 | }, 124 | connectionType: connectionTypeConsumer, 125 | consumptionHealth: make(consumptionHealth), 126 | consumer: consumer, 127 | marshaller: marshaller, 128 | } 129 | 130 | // We open an initial channel. 131 | err := channel.open() 132 | 133 | // If the channel failed to open and the keepAlive flag is set to true, we want to retry until success. 134 | if err != nil && keepAlive { 135 | go channel.retry() 136 | } 137 | 138 | return channel 139 | } 140 | 141 | // newPublishingChannel instantiates a new publishingChannel and amqpChannel for method inheritance. 142 | // - ctx is the parent context. 143 | // - connection is the parent amqp.Connection. 144 | // - keepAlive will keep the channel alive if true. 145 | // - retryDelay defines the delay between each retry, if the keepAlive flag is set to true. 146 | // - maxRetry defines the maximum number of times a message can be retried if its consumption failed. 147 | // - publishingCacheSize is the maximum cache size of failed publishing. 148 | // - publishingCacheTTL defines the time to live for each failed publishing that was put in cache. 149 | // - logger is the parent logger. 150 | // - marshaller is the Marshaller used for encoding messages. 151 | func newPublishingChannel( 152 | ctx context.Context, 153 | connection *amqp.Connection, 154 | keepAlive bool, 155 | retryDelay time.Duration, 156 | maxRetry uint, 157 | publishingCacheSize uint64, 158 | publishingCacheTTL time.Duration, 159 | logger logger, 160 | marshaller Marshaller, 161 | ) *amqpChannel { 162 | channel := &amqpChannel{ 163 | ctx: ctx, 164 | connection: connection, 165 | keepAlive: keepAlive, 166 | retryDelay: retryDelay, 167 | logger: inheritLogger(logger, map[string]interface{}{ 168 | "context": "channel", 169 | "type": connectionTypePublisher, 170 | }), 171 | releaseLogger: &stdLogger{ 172 | logger: newLogrus(), 173 | identifier: libraryName, 174 | logFields: map[string]interface{}{ 175 | "context": "channel", 176 | "type": connectionTypePublisher, 177 | }, 178 | }, 179 | connectionType: connectionTypePublisher, 180 | publishingCache: newTTLMap[string, mqttPublishing](publishingCacheSize, publishingCacheTTL), 181 | maxRetry: maxRetry, 182 | marshaller: marshaller, 183 | } 184 | 185 | // We open an initial channel. 186 | err := channel.open() 187 | 188 | // If the channel failed to open and the keepAlive flag is set to true, we want to retry until success. 189 | if err != nil && keepAlive { 190 | go channel.retry() 191 | } 192 | 193 | return channel 194 | } 195 | 196 | // open opens a new amqp.Channel from the parent connection. 197 | func (c *amqpChannel) open() error { 198 | // If the channel is nil or closed we return an error. 199 | if c.connection == nil || c.connection.IsClosed() { 200 | err := errConnectionClosed 201 | 202 | c.logger.Error(err, "Could not open channel") 203 | 204 | return err 205 | } 206 | 207 | // We request a channel from the parent connection. 208 | channel, err := c.connection.Channel() 209 | if err != nil { 210 | c.logger.Error(err, "Could not open channel") 211 | 212 | return err 213 | } 214 | 215 | c.channel = channel 216 | 217 | c.logger.Info("Channel opened") 218 | 219 | c.onChannelOpened() 220 | 221 | // If the keepAlive flag is set to true, we activate a new guard. 222 | if c.keepAlive { 223 | go c.guard() 224 | } 225 | 226 | return nil 227 | } 228 | 229 | // reconnect will indefinitely call the open method until a connection is successfully established or the context is canceled. 230 | func (c *amqpChannel) retry() { 231 | c.logger.Debug("Retry launched") 232 | 233 | for { 234 | select { 235 | case <-c.ctx.Done(): 236 | c.logger.Debug("Retry stopped by the context") 237 | 238 | // If the context was canceled, we break out of the method. 239 | return 240 | default: 241 | // Wait for the retryDelay. 242 | time.Sleep(c.retryDelay) 243 | 244 | // If there is no channel or the current channel is closed, we open a new channel. 245 | if !c.ready() { 246 | err := c.open() 247 | // If the operation succeeds, we break the loop. 248 | if err == nil { 249 | c.logger.Debug("Retry successful") 250 | 251 | return 252 | } 253 | 254 | c.logger.Error(err, "Could not open new channel during retry") 255 | } else { 256 | // If the channel exists and is active, we break out. 257 | return 258 | } 259 | } 260 | } 261 | } 262 | 263 | // guard is a channel safeguard that listens to channel close events and re-launches the channel. 264 | func (c *amqpChannel) guard() { 265 | c.logger.Debug("Guard launched") 266 | 267 | for { 268 | select { 269 | case <-c.ctx.Done(): 270 | c.logger.Debug("Guard stopped by the context") 271 | 272 | // If the context was canceled, we break out of the method. 273 | return 274 | case err, ok := <-c.channel.NotifyClose(make(chan *amqp.Error)): 275 | if !ok { 276 | return 277 | } 278 | 279 | if err != nil { 280 | c.logger.Warn("Channel lost", logField{Key: "reason", Value: err.Reason}, logField{Key: "code", Value: err.Code}) 281 | } 282 | 283 | // If the channel was explicitly closed, we do not want to retry. 284 | if c.closed { 285 | return 286 | } 287 | 288 | c.onChannelClosed() 289 | 290 | go c.retry() 291 | 292 | return 293 | } 294 | } 295 | } 296 | 297 | // close the channel only if it is ready. 298 | func (c *amqpChannel) close() error { 299 | if c.ready() { 300 | err := c.channel.Close() 301 | if err != nil { 302 | c.logger.Error(err, "Could not close channel") 303 | 304 | return err 305 | } 306 | } 307 | 308 | c.closed = true 309 | 310 | return nil 311 | } 312 | 313 | // ready returns true if the channel exists and is not closed. 314 | func (c *amqpChannel) ready() bool { 315 | return c.channel != nil && !c.channel.IsClosed() 316 | } 317 | 318 | // healthy returns true if the channel exists and is not closed. 319 | func (c *amqpChannel) healthy() bool { 320 | if c.connectionType == connectionTypeConsumer { 321 | return c.ready() && c.consumptionHealth.IsHealthy() 322 | } 323 | 324 | return c.ready() 325 | } 326 | 327 | // onChannelOpened is called when a channel is successfully opened. 328 | func (c *amqpChannel) onChannelOpened() { 329 | if c.connectionType == connectionTypeConsumer { 330 | // We re-instantiate the consumptionContext and consumptionCancel. 331 | c.consumptionCtx, c.consumptionCancel = context.WithCancel(c.ctx) 332 | 333 | // This is just a safeguard. 334 | if c.consumer != nil { 335 | c.logger.Info("Launching consumer", logField{Key: "event", Value: "onChannelOpened"}) 336 | 337 | // If the consumer is present we want to start consuming. 338 | go c.consume() 339 | } 340 | } else { 341 | // If the publishing cache is empty, nothing to do here. 342 | if c.publishingCache == nil || c.publishingCache.Len() == 0 { 343 | return 344 | } 345 | 346 | c.logger.Info("Emptying publishing cache", logField{Key: "event", Value: "onChannelOpened"}) 347 | 348 | // For each cached unsuccessful message, we try publishing it again. 349 | c.publishingCache.ForEach(func(key string, msg mqttPublishing) { 350 | _ = c.channel.PublishWithContext(c.ctx, msg.Exchange, msg.RoutingKey, msg.Mandatory, msg.Immediate, msg.Msg) 351 | 352 | c.publishingCache.Delete(key) 353 | }) 354 | } 355 | } 356 | 357 | // onChannelClosed is called when a channel is closed. 358 | func (c *amqpChannel) onChannelClosed() { 359 | if c.connectionType == connectionTypeConsumer { 360 | c.logger.Info("Canceling consumptions", logField{Key: "event", Value: "onChannelClosed"}) 361 | 362 | // We cancel the consumptionCtx. 363 | c.consumptionCancel() 364 | } 365 | } 366 | 367 | // getID returns a unique identifier for the channel. 368 | func (c *amqpChannel) getID() string { 369 | if c.consumer == nil { 370 | return fmt.Sprintf("publisher_%s", uuid.NewString()) 371 | } 372 | 373 | return fmt.Sprintf("%s_%s", c.consumer.Name, uuid.NewString()) 374 | } 375 | 376 | // consume handles the consumption mechanism. 377 | func (c *amqpChannel) consume() { 378 | // TODO(Alex): Check if this can actually happen 379 | // If the channel is not ready, we cannot consume. 380 | if !c.ready() { 381 | c.logger.Warn("Channel not ready, cannot launch consumer") 382 | 383 | return 384 | } 385 | 386 | // TODO(Alex): Double check why setting a prefetch size greater than 0 causes an error 387 | // Set the QOS, which defines how many messages can be processed at the same time. 388 | err := c.channel.Qos(c.consumer.PrefetchCount, c.consumer.PrefetchSize, false) 389 | if err != nil { 390 | c.logger.Error(err, "Could not define QOS for consumer") 391 | 392 | return 393 | } 394 | 395 | deliveries, err := c.channel.Consume(c.consumer.Queue, c.getID(), c.consumer.AutoAck, false, false, false, nil) 396 | 397 | c.consumptionHealth.AddSubscription(c.consumer.Queue, err) 398 | 399 | if err != nil { 400 | c.logger.Error(err, "Could not consume messages") 401 | 402 | // If the queue does not exist yet, we want to force a release log with a warning for better visibility. 403 | if isErrorNotFound(err) { 404 | c.releaseLogger.Warn("Queue does not exist", logField{Key: "queue", Value: c.consumer.Queue}) 405 | } 406 | 407 | return 408 | } 409 | 410 | for { 411 | select { 412 | case <-c.consumptionCtx.Done(): 413 | return 414 | case delivery := <-deliveries: 415 | // When a queue is deleted midway, a delivery with no tag or ID is received. 416 | if delivery.DeliveryTag == 0 && delivery.MessageId == "" { 417 | c.logger.Warn("Queue has been deleted, stopping consumer") 418 | 419 | return 420 | } 421 | 422 | // We copy the delivery for the concurrent process of it (otherwise we may process the wrong delivery 423 | // if a new one is consumed while the previous is still being processed). 424 | loopDelivery := delivery 425 | 426 | if c.consumer.ConcurrentProcess { 427 | // We process the message asynchronously if the concurrency is set to true. 428 | go c.processDelivery(&loopDelivery) 429 | } else { 430 | // Otherwise, we process the message synchronously. 431 | c.processDelivery(&loopDelivery) 432 | } 433 | } 434 | } 435 | } 436 | 437 | // processDelivery is the logic that defines what to do with a processed delivery and its error. 438 | func (c *amqpChannel) processDelivery(delivery *amqp.Delivery) { 439 | handler := c.consumer.Handlers.FindFunc(delivery.RoutingKey) 440 | 441 | // If the handler doesn't exist for the received delivery, we negative acknowledge it without requeue. 442 | if handler == nil { 443 | c.logger.Debug("No handler found", logField{Key: "routingKey", Value: delivery.RoutingKey}) 444 | 445 | // If the consumer is not set to auto acknowledge the delivery, we negative acknowledge it without requeue. 446 | if !c.consumer.AutoAck { 447 | _ = delivery.Nack(false, false) 448 | } 449 | 450 | return 451 | } 452 | 453 | err := handler(delivery.Body) 454 | 455 | // If the consumer has the autoAck flag activated, we want to retry the delivery in case of an error. 456 | if c.consumer.AutoAck { 457 | if err != nil { 458 | go c.retryDelivery(delivery, true) 459 | } 460 | 461 | return 462 | } 463 | 464 | // If there is no error, we can simply acknowledge the delivery. 465 | if err == nil { 466 | c.logger.Debug("Delivery successfully processed", logField{Key: "messageID", Value: delivery.MessageId}) 467 | 468 | _ = delivery.Ack(false) 469 | 470 | return 471 | } 472 | 473 | // Otherwise we retry the delivery. 474 | go c.retryDelivery(delivery, false) 475 | } 476 | 477 | // retryDelivery processes a delivery retry based on its redelivery header. 478 | // 479 | //nolint:gocognit // We can allow the current complexity for now but we should revisit it later. 480 | func (c *amqpChannel) retryDelivery(delivery *amqp.Delivery, alreadyAcknowledged bool) { 481 | c.logger.Debug("Delivery retry launched") 482 | 483 | for { 484 | select { 485 | case <-c.consumptionCtx.Done(): 486 | c.logger.Debug("Delivery retry stopped by the consumption context") 487 | 488 | return 489 | default: 490 | // We wait for the retry delay before retrying a message. 491 | time.Sleep(c.retryDelay) 492 | 493 | // We first extract the xDeathCountHeader. 494 | maxRetryHeader, exists := delivery.Headers[xDeathCountHeader] 495 | 496 | // If the header doesn't exist. 497 | if !exists { 498 | c.logger.Debug("Delivery retry invalid") 499 | 500 | // We negative acknowledge the delivery without requeue if the autoAck flag is set to false. 501 | if !alreadyAcknowledged { 502 | _ = delivery.Nack(false, false) 503 | } 504 | 505 | return 506 | } 507 | 508 | // We then cast the value as an int32. 509 | retriesCount, ok := maxRetryHeader.(int32) 510 | 511 | // If the casting fails,we negative acknowledge the delivery without requeue if the autoAck flag is set to false. 512 | if !ok { 513 | c.logger.Debug("Delivery retry invalid") 514 | 515 | if !alreadyAcknowledged { 516 | _ = delivery.Nack(false, false) 517 | } 518 | 519 | return 520 | } 521 | 522 | // If the retries count is still greater than 0, we re-publish the delivery with a decremented xDeathCountHeader. 523 | if retriesCount > 0 { 524 | c.logger.Debug("Retrying delivery", logField{Key: "retriesLeft", Value: retriesCount - 1}) 525 | 526 | // We first negative acknowledge the existing delivery to remove it from queue if the autoAck flag is set to false. 527 | if !alreadyAcknowledged { 528 | _ = delivery.Nack(false, false) 529 | } 530 | 531 | // We create a new publishing which is a copy of the old one but with a decremented xDeathCountHeader. 532 | newPublishing := amqp.Publishing{ 533 | ContentType: c.marshaller.ContentType(), 534 | Body: delivery.Body, 535 | Type: delivery.RoutingKey, 536 | Priority: delivery.Priority, 537 | DeliveryMode: delivery.DeliveryMode, 538 | MessageId: delivery.MessageId, 539 | Timestamp: delivery.Timestamp, 540 | Headers: map[string]interface{}{ 541 | xDeathCountHeader: int(retriesCount - 1), 542 | }, 543 | } 544 | 545 | // We work on a best-effort basis. We try to re-publish the delivery, but we do nothing if it fails. 546 | _ = c.channel.PublishWithContext(c.ctx, delivery.Exchange, delivery.RoutingKey, false, false, newPublishing) 547 | 548 | return 549 | } 550 | 551 | c.logger.Debug("Cannot retry delivery, max retries reached") 552 | 553 | // Otherwise, we negative acknowledge the delivery without requeue if the autoAck flag is set to false. 554 | if !alreadyAcknowledged { 555 | _ = delivery.Nack(false, false) 556 | } 557 | 558 | return 559 | } 560 | } 561 | } 562 | 563 | // publish will publish a message with the given configuration. 564 | func (c *amqpChannel) publish(exchange string, routingKey string, payload []byte, options *PublishingOptions) error { 565 | publishing := &amqp.Publishing{ 566 | ContentType: c.marshaller.ContentType(), 567 | Body: payload, 568 | Type: routingKey, 569 | Priority: PriorityMedium.Uint8(), 570 | DeliveryMode: Persistent.Uint8(), 571 | MessageId: uuid.NewString(), 572 | Timestamp: time.Now(), 573 | Headers: map[string]interface{}{ 574 | xDeathCountHeader: int(c.maxRetry), 575 | }, 576 | } 577 | 578 | // If options are declared, we add the option. 579 | if options != nil { 580 | publishing.Priority = options.priority() 581 | publishing.DeliveryMode = options.mode() 582 | publishing.Expiration = options.ttl() 583 | } 584 | 585 | // If the channel is not ready, we cannot publish, but we send the message to cache if the keepAlive flag is set to true. 586 | if !c.ready() { 587 | err := errChannelClosed 588 | 589 | if c.keepAlive { 590 | c.logger.Error(err, "Could not publish message, sending to cache") 591 | 592 | msg := mqttPublishing{ 593 | Exchange: exchange, 594 | RoutingKey: routingKey, 595 | Mandatory: false, 596 | Immediate: false, 597 | Msg: *publishing, 598 | } 599 | 600 | c.publishingCache.Put(msg.HashCode(), msg) 601 | } else { 602 | c.logger.Error(err, "Could not publish message") 603 | } 604 | 605 | return err 606 | } 607 | 608 | err := c.channel.PublishWithContext(c.ctx, exchange, routingKey, false, false, *publishing) 609 | 610 | // If the message could not be sent we return an error without caching it. 611 | if err != nil { 612 | c.logger.Error(err, "Could not publish message") 613 | 614 | // If the exchange does not exist yet, we want to force a release log with a warning for better visibility. 615 | if isErrorNotFound(err) { 616 | c.releaseLogger.Warn( 617 | "The MQTT message was not sent, exchange does not exist", 618 | logField{Key: "exchange", Value: exchange}, 619 | logField{Key: "routingKey", Value: routingKey}, 620 | ) 621 | } 622 | 623 | return err 624 | } 625 | 626 | c.logger.Debug("Message successfully sent", logField{Key: "messageID", Value: publishing.MessageId}) 627 | 628 | return nil 629 | } 630 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package gorabbit 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | ) 8 | 9 | // MQTTClient is a simple MQTT interface that offers basic client operations such as: 10 | // - Publishing 11 | // - Consuming 12 | // - Disconnecting 13 | // - Ready and health checks 14 | type MQTTClient interface { 15 | // Disconnect launches the disconnection process. 16 | // This operation disables to client permanently. 17 | Disconnect() error 18 | 19 | // Publish will send the desired payload through the selected channel. 20 | // - exchange is the name of the exchange targeted for event publishing. 21 | // - routingKey is the route that the exchange will use to forward the message. 22 | // - payload is the object you want to send as a byte array. 23 | // Returns an error if the connection to the RabbitMQ server is down. 24 | Publish(exchange, routingKey string, payload interface{}) error 25 | 26 | // PublishWithOptions will send the desired payload through the selected channel. 27 | // - exchange is the name of the exchange targeted for event publishing. 28 | // - routingKey is the route that the exchange will use to forward the message. 29 | // - payload is the object you want to send as a byte array. 30 | // Optionally you can add publishingOptions for extra customization. 31 | // Returns an error if the connection to the RabbitMQ server is down. 32 | PublishWithOptions(exchange, routingKey string, payload interface{}, options *PublishingOptions) error 33 | 34 | // RegisterConsumer will register a MessageConsumer for internal queue subscription and message processing. 35 | // The MessageConsumer will hold a list of MQTTMessageHandlers to internalize message processing. 36 | // Based on the return of error of each handler, the process of acknowledgment, rejection and retry of messages is 37 | // fully handled internally. 38 | // Furthermore, connection lost and channel errors are also internally handled by the connectionManager that will keep consumers 39 | // alive if and when necessary. 40 | RegisterConsumer(consumer MessageConsumer) error 41 | 42 | // IsReady returns true if the client is fully operational and connected to the RabbitMQ. 43 | IsReady() bool 44 | 45 | // IsHealthy returns true if the client is ready (IsReady) and all channels are operating successfully. 46 | IsHealthy() bool 47 | 48 | // GetHost returns the host used to initialize the client. 49 | GetHost() string 50 | 51 | // GetPort returns the port used to initialize the client. 52 | GetPort() uint 53 | 54 | // GetUsername returns the username used to initialize the client. 55 | GetUsername() string 56 | 57 | // GetVhost returns the vhost used to initialize the client. 58 | GetVhost() string 59 | 60 | // IsDisabled returns whether the client is disabled or not. 61 | IsDisabled() bool 62 | } 63 | 64 | type mqttClient struct { 65 | // Host is the RabbitMQ server host name. 66 | Host string 67 | 68 | // Port is the RabbitMQ server port number. 69 | Port uint 70 | 71 | // Username is the RabbitMQ server allowed username. 72 | Username string 73 | 74 | // Password is the RabbitMQ server allowed password. 75 | Password string 76 | 77 | // Vhost is used for CloudAMQP connections to set the specific vhost. 78 | Vhost string 79 | 80 | // logger defines the logger used, depending on the mode set. 81 | logger logger 82 | 83 | // disabled completely disables the client if true. 84 | disabled bool 85 | 86 | // connectionManager manages the connection and channel logic and high-level logic 87 | // such as keep alive mechanism and health check. 88 | connectionManager *connectionManager 89 | 90 | // ctx holds the global context used for the client. 91 | ctx context.Context 92 | 93 | // cancel is the cancelFunc for the ctx. 94 | cancel context.CancelFunc 95 | } 96 | 97 | // NewClient will instantiate a new MQTTClient. 98 | // If options is set to nil, the DefaultClientOptions will be used. 99 | func NewClient(options *ClientOptions) MQTTClient { 100 | // If no options is passed, we use the DefaultClientOptions. 101 | if options == nil { 102 | options = DefaultClientOptions() 103 | } 104 | 105 | return newClientFromOptions(options) 106 | } 107 | 108 | // NewClientFromEnv will instantiate a new MQTTClient from environment variables. 109 | func NewClientFromEnv() MQTTClient { 110 | options := NewClientOptionsFromEnv() 111 | 112 | return newClientFromOptions(options) 113 | } 114 | 115 | func newClientFromOptions(options *ClientOptions) MQTTClient { 116 | client := &mqttClient{ 117 | Host: options.Host, 118 | Port: options.Port, 119 | Username: options.Username, 120 | Password: options.Password, 121 | Vhost: options.Vhost, 122 | logger: &noLogger{}, 123 | } 124 | 125 | // We check if the disabled flag is present, which will completely disable the MQTTClient. 126 | if disabledOverride := os.Getenv("GORABBIT_DISABLED"); disabledOverride != "" { 127 | switch disabledOverride { 128 | case "1", "true": 129 | client.disabled = true 130 | 131 | return client 132 | } 133 | } 134 | 135 | // We check if the mode was overwritten with the environment variable "GORABBIT_MODE". 136 | if modeOverride := os.Getenv("GORABBIT_MODE"); isValidMode(modeOverride) { 137 | // We override the mode only if it is valid 138 | options.Mode = modeOverride 139 | } 140 | 141 | if options.Mode == Debug { 142 | // If the mode is Debug, we want to actually log important events. 143 | client.logger = newStdLogger() 144 | } 145 | 146 | client.ctx, client.cancel = context.WithCancel(context.Background()) 147 | 148 | protocol := defaultProtocol 149 | 150 | if options.UseTLS { 151 | protocol = securedProtocol 152 | } 153 | 154 | if options.Marshaller == nil { 155 | options.Marshaller = defaultMarshaller 156 | } 157 | 158 | dialURL := fmt.Sprintf("%s://%s:%s@%s:%d/%s", protocol, client.Username, client.Password, client.Host, client.Port, client.Vhost) 159 | 160 | client.connectionManager = newConnectionManager( 161 | client.ctx, 162 | dialURL, 163 | options.ConnectionName, 164 | options.KeepAlive, 165 | options.RetryDelay, 166 | options.MaxRetry, 167 | options.PublishingCacheSize, 168 | options.PublishingCacheTTL, 169 | client.logger, 170 | options.Marshaller, 171 | ) 172 | 173 | return client 174 | } 175 | 176 | func (client *mqttClient) Publish(exchange string, routingKey string, payload interface{}) error { 177 | return client.PublishWithOptions(exchange, routingKey, payload, nil) 178 | } 179 | 180 | func (client *mqttClient) PublishWithOptions(exchange string, routingKey string, payload interface{}, options *PublishingOptions) error { 181 | // client is disabled, so we do nothing and return no error. 182 | if client.disabled { 183 | return nil 184 | } 185 | 186 | return client.connectionManager.publish(exchange, routingKey, payload, options) 187 | } 188 | 189 | func (client *mqttClient) RegisterConsumer(consumer MessageConsumer) error { 190 | // client is disabled, so we do nothing and return no error. 191 | if client.disabled { 192 | return nil 193 | } 194 | 195 | return client.connectionManager.registerConsumer(consumer) 196 | } 197 | 198 | func (client *mqttClient) Disconnect() error { 199 | // client is disabled, so we do nothing and return no error. 200 | if client.disabled { 201 | return nil 202 | } 203 | 204 | err := client.connectionManager.close() 205 | 206 | if err != nil { 207 | return err 208 | } 209 | 210 | // cancel the context to stop all reconnection goroutines. 211 | client.cancel() 212 | 213 | // disable the client to avoid trying to launch new operations. 214 | client.disabled = true 215 | 216 | return nil 217 | } 218 | 219 | func (client *mqttClient) IsReady() bool { 220 | // client is disabled, so we do nothing and return true. 221 | if client.disabled { 222 | return true 223 | } 224 | 225 | return client.connectionManager.isReady() 226 | } 227 | 228 | func (client *mqttClient) IsHealthy() bool { 229 | // client is disabled, so we do nothing and return true. 230 | if client.disabled { 231 | return true 232 | } 233 | 234 | return client.connectionManager.isHealthy() 235 | } 236 | 237 | func (client *mqttClient) GetHost() string { 238 | return client.Host 239 | } 240 | 241 | func (client *mqttClient) GetPort() uint { 242 | return client.Port 243 | } 244 | 245 | func (client *mqttClient) GetUsername() string { 246 | return client.Username 247 | } 248 | 249 | func (client *mqttClient) GetVhost() string { 250 | return client.Vhost 251 | } 252 | 253 | func (client *mqttClient) IsDisabled() bool { 254 | return client.disabled 255 | } 256 | -------------------------------------------------------------------------------- /client_options.go: -------------------------------------------------------------------------------- 1 | package gorabbit 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/Netflix/go-env" 7 | ) 8 | 9 | // ClientOptions holds all necessary properties to launch a successful connection with an MQTTClient. 10 | type ClientOptions struct { 11 | // Host is the RabbitMQ server host name. 12 | Host string 13 | 14 | // Port is the RabbitMQ server port number. 15 | Port uint 16 | 17 | // Username is the RabbitMQ server allowed username. 18 | Username string 19 | 20 | // Password is the RabbitMQ server allowed password. 21 | Password string 22 | 23 | // Vhost is used for CloudAMQP connections to set the specific vhost. 24 | Vhost string 25 | 26 | // UseTLS defines whether we use amqp or amqps protocol. 27 | UseTLS bool 28 | 29 | // ConnectionName is the client connection name passed on to the RabbitMQ server. 30 | ConnectionName string 31 | 32 | // KeepAlive will determine whether the re-connection and retry mechanisms should be triggered. 33 | KeepAlive bool 34 | 35 | // RetryDelay will define the delay for the re-connection and retry mechanism. 36 | RetryDelay time.Duration 37 | 38 | // MaxRetry will define the number of retries when an amqpMessage could not be processed. 39 | MaxRetry uint 40 | 41 | // PublishingCacheTTL defines the time to live for each publishing cache item. 42 | PublishingCacheTTL time.Duration 43 | 44 | // PublishingCacheSize defines the max length of the publishing cache. 45 | PublishingCacheSize uint64 46 | 47 | // Mode will specify whether logs are enabled or not. 48 | Mode string 49 | 50 | // Marshaller defines the content type used for messages and how they're marshalled (default: JSON). 51 | Marshaller Marshaller 52 | } 53 | 54 | // DefaultClientOptions will return a ClientOptions with default values. 55 | func DefaultClientOptions() *ClientOptions { 56 | return &ClientOptions{ 57 | Host: defaultHost, 58 | Port: defaultPort, 59 | Username: defaultUsername, 60 | Password: defaultPassword, 61 | Vhost: defaultVhost, 62 | UseTLS: defaultUseTLS, 63 | KeepAlive: defaultKeepAlive, 64 | RetryDelay: defaultRetryDelay, 65 | MaxRetry: defaultMaxRetry, 66 | PublishingCacheTTL: defaultPublishingCacheTTL, 67 | PublishingCacheSize: defaultPublishingCacheSize, 68 | Mode: defaultMode, 69 | Marshaller: defaultMarshaller, 70 | } 71 | } 72 | 73 | // NewClientOptions is the exported builder for a ClientOptions and will offer setter methods for an easy construction. 74 | // Any non-assigned field will be set to default through DefaultClientOptions. 75 | func NewClientOptions() *ClientOptions { 76 | return DefaultClientOptions() 77 | } 78 | 79 | // NewClientOptionsFromEnv will generate a ClientOptions from environment variables. Empty values will be taken as default 80 | // through the DefaultClientOptions. 81 | func NewClientOptionsFromEnv() *ClientOptions { 82 | defaultOpts := DefaultClientOptions() 83 | 84 | fromEnv := new(RabbitMQEnvs) 85 | 86 | _, err := env.UnmarshalFromEnviron(fromEnv) 87 | if err != nil { 88 | return defaultOpts 89 | } 90 | 91 | if fromEnv.Host != "" { 92 | defaultOpts.Host = fromEnv.Host 93 | } 94 | 95 | if fromEnv.Port > 0 { 96 | defaultOpts.Port = fromEnv.Port 97 | } 98 | 99 | if fromEnv.Username != "" { 100 | defaultOpts.Username = fromEnv.Username 101 | } 102 | 103 | if fromEnv.Password != "" { 104 | defaultOpts.Password = fromEnv.Password 105 | } 106 | 107 | if fromEnv.Vhost != "" { 108 | defaultOpts.Vhost = fromEnv.Vhost 109 | } 110 | 111 | defaultOpts.UseTLS = fromEnv.UseTLS 112 | defaultOpts.ConnectionName = fromEnv.ConnectionName 113 | 114 | return defaultOpts 115 | } 116 | 117 | // SetHost will assign the Host. 118 | func (c *ClientOptions) SetHost(host string) *ClientOptions { 119 | c.Host = host 120 | 121 | return c 122 | } 123 | 124 | // SetPort will assign the Port. 125 | func (c *ClientOptions) SetPort(port uint) *ClientOptions { 126 | c.Port = port 127 | 128 | return c 129 | } 130 | 131 | // SetCredentials will assign the Username and Password. 132 | func (c *ClientOptions) SetCredentials(username, password string) *ClientOptions { 133 | c.Username = username 134 | c.Password = password 135 | 136 | return c 137 | } 138 | 139 | // SetVhost will assign the Vhost. 140 | func (c *ClientOptions) SetVhost(vhost string) *ClientOptions { 141 | c.Vhost = vhost 142 | 143 | return c 144 | } 145 | 146 | // SetUseTLS will assign the UseTLS status. 147 | func (c *ClientOptions) SetUseTLS(use bool) *ClientOptions { 148 | c.UseTLS = use 149 | 150 | return c 151 | } 152 | 153 | // SetConnectionName will assign the ConnectionName. 154 | func (c *ClientOptions) SetConnectionName(connectionName string) *ClientOptions { 155 | c.ConnectionName = connectionName 156 | 157 | return c 158 | } 159 | 160 | // SetKeepAlive will assign the KeepAlive status. 161 | func (c *ClientOptions) SetKeepAlive(keepAlive bool) *ClientOptions { 162 | c.KeepAlive = keepAlive 163 | 164 | return c 165 | } 166 | 167 | // SetRetryDelay will assign the retry delay. 168 | func (c *ClientOptions) SetRetryDelay(delay time.Duration) *ClientOptions { 169 | c.RetryDelay = delay 170 | 171 | return c 172 | } 173 | 174 | // SetMaxRetry will assign the max retry count. 175 | func (c *ClientOptions) SetMaxRetry(retry uint) *ClientOptions { 176 | c.MaxRetry = retry 177 | 178 | return c 179 | } 180 | 181 | // SetPublishingCacheTTL will assign the publishing cache item TTL. 182 | func (c *ClientOptions) SetPublishingCacheTTL(ttl time.Duration) *ClientOptions { 183 | c.PublishingCacheTTL = ttl 184 | 185 | return c 186 | } 187 | 188 | // SetPublishingCacheSize will assign the publishing cache max length. 189 | func (c *ClientOptions) SetPublishingCacheSize(size uint64) *ClientOptions { 190 | c.PublishingCacheSize = size 191 | 192 | return c 193 | } 194 | 195 | // SetMode will assign the Mode if valid. 196 | func (c *ClientOptions) SetMode(mode string) *ClientOptions { 197 | if isValidMode(mode) { 198 | c.Mode = mode 199 | } 200 | 201 | return c 202 | } 203 | 204 | // SetMarshaller will assign the Marshaller. 205 | func (c *ClientOptions) SetMarshaller(marshaller Marshaller) *ClientOptions { 206 | c.Marshaller = marshaller 207 | 208 | return c 209 | } 210 | -------------------------------------------------------------------------------- /connection.go: -------------------------------------------------------------------------------- 1 | package gorabbit 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/url" 7 | "time" 8 | 9 | "github.com/google/uuid" 10 | amqp "github.com/rabbitmq/amqp091-go" 11 | ) 12 | 13 | // amqpConnection holds information about the management of the native amqp.Connection. 14 | type amqpConnection struct { 15 | // ctx is the parent context and acts as a safeguard. 16 | ctx context.Context 17 | 18 | // connection is the native amqp.Connection. 19 | connection *amqp.Connection 20 | 21 | // uri represents the connection string to the RabbitMQ server. 22 | uri string 23 | 24 | // connectionName is the client connection name passed on to the RabbitMQ server. 25 | connectionName string 26 | 27 | // keepAlive is the flag that will define whether active guards and re-connections are enabled or not. 28 | keepAlive bool 29 | 30 | // retryDelay defines the delay to wait before re-connecting if we lose connection and the keepAlive flag is set to true. 31 | retryDelay time.Duration 32 | 33 | // closed is an inner property that switches to true if the connection was explicitly closed. 34 | closed bool 35 | 36 | // channels holds a list of active amqpChannel 37 | channels amqpChannels 38 | 39 | // maxRetry defines the number of retries when publishing a message. 40 | maxRetry uint 41 | 42 | // publishingCacheSize defines the maximum length of cached failed publishing. 43 | publishingCacheSize uint64 44 | 45 | // publishingCacheTTL defines the time to live for a cached failed publishing. 46 | publishingCacheTTL time.Duration 47 | 48 | // logger logs events. 49 | logger logger 50 | 51 | // connectionType defines the connectionType. 52 | connectionType connectionType 53 | 54 | // marshaller defines the marshalling method used to encode messages. 55 | marshaller Marshaller 56 | } 57 | 58 | // newConsumerConnection initializes a new consumer amqpConnection with given arguments. 59 | // - ctx is the parent context. 60 | // - uri is the connection string. 61 | // - connectionName is the connection name. 62 | // - keepAlive will keep the connection alive if true. 63 | // - retryDelay defines the delay between each re-connection, if the keepAlive flag is set to true. 64 | // - logger is the parent logger. 65 | // - marshaller is the Marshaller used for encoding messages. 66 | func newConsumerConnection( 67 | ctx context.Context, 68 | uri string, 69 | connectionName string, 70 | keepAlive bool, 71 | retryDelay time.Duration, 72 | logger logger, 73 | marshaller Marshaller, 74 | ) *amqpConnection { 75 | connectionName = fmt.Sprintf("%s-consumer-%s", connectionName, uuid.NewString()) 76 | 77 | return newConnection(ctx, uri, connectionName, keepAlive, retryDelay, logger, connectionTypeConsumer, marshaller) 78 | } 79 | 80 | // newPublishingConnection initializes a new publisher amqpConnection with given arguments. 81 | // - ctx is the parent context. 82 | // - uri is the connection string. 83 | // - connectionName is the connection name. 84 | // - keepAlive will keep the connection alive if true. 85 | // - retryDelay defines the delay between each re-connection, if the keepAlive flag is set to true. 86 | // - maxRetry defines the publishing max retry header. 87 | // - publishingCacheSize defines the maximum length of failed publishing cache. 88 | // - publishingCacheTTL defines the time to live for failed publishing in cache. 89 | // - logger is the parent logger. 90 | // - marshaller is the Marshaller used for encoding messages. 91 | func newPublishingConnection( 92 | ctx context.Context, 93 | uri string, 94 | connectionName string, 95 | keepAlive bool, 96 | retryDelay time.Duration, 97 | maxRetry uint, 98 | publishingCacheSize uint64, 99 | publishingCacheTTL time.Duration, 100 | logger logger, 101 | marshaller Marshaller, 102 | ) *amqpConnection { 103 | connectionName = fmt.Sprintf("%s-publisher-%s", connectionName, uuid.NewString()) 104 | 105 | conn := newConnection(ctx, uri, connectionName, keepAlive, retryDelay, logger, connectionTypePublisher, marshaller) 106 | 107 | conn.maxRetry = maxRetry 108 | conn.publishingCacheSize = publishingCacheSize 109 | conn.publishingCacheTTL = publishingCacheTTL 110 | 111 | return conn 112 | } 113 | 114 | // newConnection initializes a new amqpConnection with given arguments. 115 | // - ctx is the parent context. 116 | // - uri is the connection string. 117 | // - connectionName is the connection name. 118 | // - keepAlive will keep the connection alive if true. 119 | // - retryDelay defines the delay between each re-connection, if the keepAlive flag is set to true. 120 | // - logger is the parent logger. 121 | // - marshaller is the Marshaller used for encoding messages. 122 | func newConnection( 123 | ctx context.Context, 124 | uri string, 125 | connectionName string, 126 | keepAlive bool, 127 | retryDelay time.Duration, 128 | logger logger, 129 | connectionType connectionType, 130 | marshaller Marshaller, 131 | ) *amqpConnection { 132 | conn := &amqpConnection{ 133 | ctx: ctx, 134 | uri: uri, 135 | connectionName: connectionName, 136 | keepAlive: keepAlive, 137 | retryDelay: retryDelay, 138 | channels: make(amqpChannels, 0), 139 | logger: inheritLogger(logger, map[string]interface{}{ 140 | "context": "connection", 141 | "type": connectionType, 142 | }), 143 | connectionType: connectionType, 144 | marshaller: marshaller, 145 | } 146 | 147 | conn.logger.Debug("Initializing new amqp connection", logField{Key: "uri", Value: conn.uriForLog()}) 148 | 149 | // We open an initial connection. 150 | err := conn.open() 151 | 152 | // If the connection failed and the keepAlive flag is set to true, we want to re-connect until success. 153 | if err != nil && keepAlive { 154 | go conn.reconnect() 155 | } 156 | 157 | return conn 158 | } 159 | 160 | // open opens a new amqp.Connection with the help of a defined uri. 161 | func (a *amqpConnection) open() error { 162 | // If the uri is empty, we return an error. 163 | if a.uri == "" { 164 | return errEmptyURI 165 | } 166 | 167 | a.logger.Debug("Connecting to RabbitMQ server", logField{Key: "uri", Value: a.uriForLog()}) 168 | 169 | props := amqp.NewConnectionProperties() 170 | if a.connectionName != "" { 171 | props.SetClientConnectionName(a.connectionName) 172 | } 173 | 174 | // We request a connection from the RabbitMQ server. 175 | conn, err := amqp.DialConfig(a.uri, amqp.Config{ 176 | Heartbeat: defaultHeartbeat, 177 | Locale: defaultLocale, 178 | Properties: props, 179 | }) 180 | if err != nil { 181 | a.logger.Error(err, "Connection failed") 182 | 183 | return err 184 | } 185 | 186 | a.logger.Info("Connection successful", logField{Key: "uri", Value: a.uriForLog()}) 187 | 188 | a.connection = conn 189 | 190 | a.channels.updateParentConnection(a.connection) 191 | 192 | // If the keepAlive flag is set to true, we activate a new guard. 193 | if a.keepAlive { 194 | go a.guard() 195 | } 196 | 197 | return nil 198 | } 199 | 200 | // reconnect will indefinitely call the open method until a connection is successfully established or the context is canceled. 201 | func (a *amqpConnection) reconnect() { 202 | a.logger.Debug("Re-connection launched") 203 | 204 | for { 205 | select { 206 | case <-a.ctx.Done(): 207 | a.logger.Debug("Re-connection stopped by the context") 208 | 209 | // If the context was canceled, we break out of the method. 210 | return 211 | default: 212 | // Wait for the retryDelay. 213 | time.Sleep(a.retryDelay) 214 | 215 | // If there is no connection or the current connection is closed, we open a new connection. 216 | if !a.ready() { 217 | err := a.open() 218 | // If the operation succeeds, we break the loop. 219 | if err == nil { 220 | a.logger.Debug("Re-connection successful") 221 | 222 | return 223 | } 224 | 225 | a.logger.Error(err, "Could not open new connection during re-connection") 226 | } else { 227 | // If the connection exists and is active, we break out. 228 | return 229 | } 230 | } 231 | } 232 | } 233 | 234 | // guard is a connection safeguard that listens to connection close events and re-launches the connection. 235 | func (a *amqpConnection) guard() { 236 | a.logger.Debug("Guard launched") 237 | 238 | for { 239 | select { 240 | case <-a.ctx.Done(): 241 | a.logger.Debug("Guard stopped by the context") 242 | 243 | // If the context was canceled, we break out of the method. 244 | return 245 | case err, ok := <-a.connection.NotifyClose(make(chan *amqp.Error)): 246 | if !ok { 247 | return 248 | } 249 | 250 | if err != nil { 251 | a.logger.Warn("Connection lost", logField{Key: "reason", Value: err.Reason}, logField{Key: "code", Value: err.Code}) 252 | } 253 | 254 | // If the connection was explicitly closed, we do not want to re-connect. 255 | if a.closed { 256 | return 257 | } 258 | 259 | go a.reconnect() 260 | 261 | return 262 | } 263 | } 264 | } 265 | 266 | // close the connection only if it is ready. 267 | func (a *amqpConnection) close() error { 268 | if a.ready() { 269 | for _, channel := range a.channels { 270 | err := channel.close() 271 | if err != nil { 272 | return err 273 | } 274 | } 275 | 276 | err := a.connection.Close() 277 | if err != nil { 278 | a.logger.Error(err, "Could not close connection") 279 | 280 | return err 281 | } 282 | } 283 | 284 | a.closed = true 285 | 286 | a.logger.Info("Connection closed") 287 | 288 | return nil 289 | } 290 | 291 | // ready returns true if the connection exists and is not closed. 292 | func (a *amqpConnection) ready() bool { 293 | return a.connection != nil && !a.connection.IsClosed() 294 | } 295 | 296 | // healthy returns true if the connection exists, is not closed and all child channels are healthy. 297 | func (a *amqpConnection) healthy() bool { 298 | // If the connection is not ready, return false. 299 | if !a.ready() { 300 | return false 301 | } 302 | 303 | // Verify that all connection channels are ready too. 304 | for _, channel := range a.channels { 305 | if !channel.healthy() { 306 | return false 307 | } 308 | } 309 | 310 | return true 311 | } 312 | 313 | // registerConsumer opens a new consumerChannel and registers the MessageConsumer. 314 | func (a *amqpConnection) registerConsumer(consumer MessageConsumer) error { 315 | for _, channel := range a.channels { 316 | if channel.consumer != nil && channel.consumer.Queue == consumer.Queue { 317 | err := errConsumerAlreadyExists 318 | 319 | a.logger.Error(err, "Could not register consumer", logField{Key: "consumer", Value: consumer.Name}) 320 | 321 | return err 322 | } 323 | } 324 | 325 | if err := consumer.Handlers.Validate(); err != nil { 326 | return err 327 | } 328 | 329 | channel := newConsumerChannel(a.ctx, a.connection, a.keepAlive, a.retryDelay, &consumer, a.logger, a.marshaller) 330 | 331 | a.channels = append(a.channels, channel) 332 | 333 | a.logger.Info("Consumer registered", logField{Key: "consumer", Value: consumer.Name}) 334 | 335 | return nil 336 | } 337 | 338 | func (a *amqpConnection) publish(exchange, routingKey string, payload []byte, options *PublishingOptions) error { 339 | publishingChannel := a.channels.publishingChannel() 340 | if publishingChannel == nil { 341 | publishingChannel = newPublishingChannel( 342 | a.ctx, a.connection, a.keepAlive, a.retryDelay, a.maxRetry, 343 | a.publishingCacheSize, a.publishingCacheTTL, a.logger, a.marshaller, 344 | ) 345 | 346 | a.channels = append(a.channels, publishingChannel) 347 | } 348 | 349 | return publishingChannel.publish(exchange, routingKey, payload, options) 350 | } 351 | 352 | // uriForLog returns the uri with the password hidden for security measures. 353 | func (a *amqpConnection) uriForLog() string { 354 | if a.uri == "" { 355 | return a.uri 356 | } 357 | 358 | parsedURL, err := url.Parse(a.uri) 359 | if err != nil { 360 | return "" 361 | } 362 | 363 | hiddenPassword := "xxxx" 364 | 365 | if parsedURL.User != nil { 366 | parsedURL.User = url.UserPassword(parsedURL.User.Username(), hiddenPassword) 367 | } 368 | 369 | return parsedURL.String() 370 | } 371 | -------------------------------------------------------------------------------- /connection_manager.go: -------------------------------------------------------------------------------- 1 | package gorabbit 2 | 3 | import ( 4 | "context" 5 | "time" 6 | ) 7 | 8 | type connectionManager struct { 9 | // consumerConnection holds the independent consuming connection. 10 | consumerConnection *amqpConnection 11 | 12 | // publisherConnection holds the independent publishing connection. 13 | publisherConnection *amqpConnection 14 | 15 | // marshaller holds the marshaller used to encode messages. 16 | marshaller Marshaller 17 | } 18 | 19 | // newConnectionManager instantiates a new connectionManager with given arguments. 20 | func newConnectionManager( 21 | ctx context.Context, 22 | uri string, 23 | connectionName string, 24 | keepAlive bool, 25 | retryDelay time.Duration, 26 | maxRetry uint, 27 | publishingCacheSize uint64, 28 | publishingCacheTTL time.Duration, 29 | logger logger, 30 | marshaller Marshaller, 31 | ) *connectionManager { 32 | if connectionName == "" { 33 | connectionName = libraryName 34 | } 35 | 36 | c := &connectionManager{ 37 | consumerConnection: newConsumerConnection( 38 | ctx, uri, connectionName, keepAlive, retryDelay, logger, marshaller, 39 | ), 40 | publisherConnection: newPublishingConnection( 41 | ctx, uri, connectionName, keepAlive, retryDelay, maxRetry, 42 | publishingCacheSize, publishingCacheTTL, logger, marshaller, 43 | ), 44 | marshaller: marshaller, 45 | } 46 | 47 | return c 48 | } 49 | 50 | // close offers the basic connection and channel close() mechanism but with extra higher level checks. 51 | func (c *connectionManager) close() error { 52 | if err := c.publisherConnection.close(); err != nil { 53 | return err 54 | } 55 | 56 | return c.consumerConnection.close() 57 | } 58 | 59 | // isReady returns true if both consumerConnection and publishingConnection are ready. 60 | func (c *connectionManager) isReady() bool { 61 | if c.publisherConnection == nil || c.consumerConnection == nil { 62 | return false 63 | } 64 | 65 | return c.publisherConnection.ready() && c.consumerConnection.ready() 66 | } 67 | 68 | // isHealthy returns true if both consumerConnection and publishingConnection are healthy. 69 | func (c *connectionManager) isHealthy() bool { 70 | if c.publisherConnection == nil || c.consumerConnection == nil { 71 | return false 72 | } 73 | 74 | return c.publisherConnection.healthy() && c.consumerConnection.healthy() 75 | } 76 | 77 | // registerConsumer registers a new MessageConsumer. 78 | func (c *connectionManager) registerConsumer(consumer MessageConsumer) error { 79 | if c.consumerConnection == nil { 80 | return errConsumerConnectionNotInitialized 81 | } 82 | 83 | return c.consumerConnection.registerConsumer(consumer) 84 | } 85 | 86 | func (c *connectionManager) publish(exchange, routingKey string, payload interface{}, options *PublishingOptions) error { 87 | if c.publisherConnection == nil { 88 | return errPublisherConnectionNotInitialized 89 | } 90 | 91 | payloadBytes, err := c.marshaller.Marshal(payload) 92 | if err != nil { 93 | return err 94 | } 95 | 96 | return c.publisherConnection.publish(exchange, routingKey, payloadBytes, options) 97 | } 98 | -------------------------------------------------------------------------------- /constants.go: -------------------------------------------------------------------------------- 1 | package gorabbit 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | ) 7 | 8 | // Library name. 9 | const libraryName = "Gorabbit" 10 | 11 | // Connection protocols. 12 | const ( 13 | defaultProtocol = "amqp" 14 | securedProtocol = "amqps" 15 | ) 16 | 17 | // Default values for the ClientOptions and ManagerOptions. 18 | const ( 19 | defaultHost = "127.0.0.1" 20 | defaultPort = 5672 21 | defaultUsername = "guest" 22 | defaultPassword = "guest" 23 | defaultVhost = "" 24 | defaultUseTLS = false 25 | defaultKeepAlive = true 26 | defaultRetryDelay = 3 * time.Second 27 | defaultMaxRetry = 5 28 | defaultPublishingCacheTTL = 60 * time.Second 29 | defaultPublishingCacheSize = 128 30 | defaultMode = Release 31 | ) 32 | 33 | var defaultMarshaller = NewJSONMarshaller() 34 | 35 | // Default values for the amqp Config. 36 | const ( 37 | defaultHeartbeat = 10 * time.Second 38 | defaultLocale = "en_US" 39 | ) 40 | 41 | const ( 42 | xDeathCountHeader = "x-death-count" 43 | ) 44 | 45 | // Connection Types. 46 | 47 | type connectionType string 48 | 49 | const ( 50 | connectionTypeConsumer connectionType = "consumer" 51 | connectionTypePublisher connectionType = "publisher" 52 | ) 53 | 54 | // Exchange Types 55 | 56 | type ExchangeType string 57 | 58 | const ( 59 | ExchangeTypeTopic ExchangeType = "topic" 60 | ExchangeTypeDirect ExchangeType = "direct" 61 | ExchangeTypeFanout ExchangeType = "fanout" 62 | ExchangeTypeHeaders ExchangeType = "headers" 63 | ) 64 | 65 | func (e ExchangeType) String() string { 66 | return string(e) 67 | } 68 | 69 | // Priority Levels. 70 | 71 | type MessagePriority uint8 72 | 73 | const ( 74 | PriorityLowest MessagePriority = 1 75 | PriorityVeryLow MessagePriority = 2 76 | PriorityLow MessagePriority = 3 77 | PriorityMedium MessagePriority = 4 78 | PriorityHigh MessagePriority = 5 79 | PriorityHighest MessagePriority = 6 80 | ) 81 | 82 | func (m MessagePriority) Uint8() uint8 { 83 | return uint8(m) 84 | } 85 | 86 | // Delivery Modes. 87 | 88 | type DeliveryMode uint8 89 | 90 | const ( 91 | Transient DeliveryMode = 1 92 | Persistent DeliveryMode = 2 93 | ) 94 | 95 | func (d DeliveryMode) Uint8() uint8 { 96 | return uint8(d) 97 | } 98 | 99 | // Logging Modes. 100 | const ( 101 | Release = "release" 102 | Debug = "debug" 103 | ) 104 | 105 | func isValidMode(mode string) bool { 106 | return mode == Release || mode == Debug 107 | } 108 | 109 | // Errors. 110 | var ( 111 | errEmptyURI = errors.New("amqp uri is empty") 112 | errChannelClosed = errors.New("channel is closed") 113 | errConnectionClosed = errors.New("connection is closed") 114 | errConsumerAlreadyExists = errors.New("consumer already exists") 115 | errConsumerConnectionNotInitialized = errors.New("consumerConnection is not initialized") 116 | errPublisherConnectionNotInitialized = errors.New("publisherConnection is not initialized") 117 | errEmptyQueue = errors.New("queue is empty") 118 | ) 119 | -------------------------------------------------------------------------------- /consumer.go: -------------------------------------------------------------------------------- 1 | package gorabbit 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | // MQTTMessageHandlers is a wrapper that holds a map[string]MQTTMessageHandlerFunc. 10 | type MQTTMessageHandlers map[string]MQTTMessageHandlerFunc 11 | 12 | // MQTTMessageHandlerFunc is the function that will be called when a delivery is received. 13 | type MQTTMessageHandlerFunc func(payload []byte) error 14 | 15 | // Validate verifies that all routing keys in the handlers are properly formatted and allowed. 16 | // 17 | //nolint:gocognit // We can allow the current complexity for now but we should revisit it later. 18 | func (mh MQTTMessageHandlers) Validate() error { 19 | for k := range mh { 20 | // A routing key cannot be empty. 21 | if len(k) == 0 { 22 | return errors.New("a routing key cannot be empty") 23 | } 24 | 25 | // A routing key cannot be equal to the wildcard '#'. 26 | if len(k) == 1 && k == "#" { 27 | return errors.New("a routing key cannot be the wildcard '#'") 28 | } 29 | 30 | // A routing key cannot contain spaces. 31 | if strings.Contains(k, " ") { 32 | return errors.New("a routing key cannot contain spaces") 33 | } 34 | 35 | // If a routing key is not just made up of one word. 36 | if strings.Contains(k, ".") { 37 | // We need to make sure that we do not find an empty word or a '%' in the middle of the key. 38 | split := strings.Split(k, ".") 39 | 40 | for i, v := range split { 41 | // We cannot have empty strings. 42 | if v == "" { 43 | return fmt.Errorf("the routing key '%s' is not properly formatted", k) 44 | } 45 | 46 | // The wildcard '#' is not allowed in the middle. 47 | if v == "#" && i > 0 && i < len(split)-1 { 48 | return fmt.Errorf("the wildcard '#' in the routing key '%s' is not allowed", k) 49 | } 50 | } 51 | } 52 | } 53 | 54 | return nil 55 | } 56 | 57 | // matchesPrefixWildcard verifies that everything that comes after the '#' wildcard matches. 58 | func (mh MQTTMessageHandlers) matchesPrefixWildcard(storedWords, words []string) bool { 59 | // compareIndex starts after the wildcard in the storedWords array. 60 | compareIndex := 1 61 | 62 | // we initialize the wordIdx at -1. 63 | wordIdx := -1 64 | 65 | // Here we are searching for the first occurrence of the first word after the '#' wildcard 66 | // of the storedWords in the words. 67 | for i, w := range words { 68 | if w == storedWords[compareIndex] { 69 | // We can now start comparing at 'i'. 70 | wordIdx = i 71 | 72 | break 73 | } 74 | } 75 | 76 | // If we did not find the first word, then surely the key does not match. 77 | if wordIdx == -1 { 78 | return false 79 | } 80 | 81 | // If the length of storedWords is not the same as the length of words after the wildcard, 82 | // then surely the key does not match. 83 | if len(storedWords)-compareIndex != len(words)-wordIdx { 84 | return false 85 | } 86 | 87 | // Now we can compare, word by word if the routing keys matches. 88 | for i := wordIdx; i < len(words); i++ { 89 | // Be careful, if we find '*' then it should match no matter what. 90 | if storedWords[compareIndex] != words[i] && storedWords[compareIndex] != "*" { 91 | return false 92 | } 93 | 94 | // We move right in the storedWords. 95 | compareIndex++ 96 | } 97 | 98 | return true 99 | } 100 | 101 | // matchesSuffixWildcard verifies that everything that comes before the '#' wildcard matches. 102 | func (mh MQTTMessageHandlers) matchesSuffixWildcard(storedWords, words []string) bool { 103 | backCount := 2 104 | 105 | // compareIndex starts before the wildcard in the storedWords array. 106 | compareIndex := len(storedWords) - backCount 107 | 108 | // we initialize the wordIdx at -1. 109 | wordIdx := -1 110 | 111 | // Here we are searching for the first occurrence of the first word before the '#' wildcard 112 | // of the storedWords in the words. 113 | for i, w := range words { 114 | if w == storedWords[compareIndex] { 115 | wordIdx = i 116 | 117 | break 118 | } 119 | } 120 | 121 | // If we did not find the first word, then surely the key does not match. 122 | if wordIdx == -1 { 123 | return false 124 | } 125 | 126 | // If the indexes are not the same then surely the key does not match. 127 | if compareIndex != wordIdx { 128 | return false 129 | } 130 | 131 | // Now we can compare, word by word, going backwards if the routing keys matches. 132 | for i := wordIdx; i > -1; i-- { 133 | // Be careful, if we find '*' then it should match no matter what. 134 | if storedWords[compareIndex] != words[i] && storedWords[compareIndex] != "*" { 135 | return false 136 | } 137 | 138 | // We move left in the storedWords. 139 | compareIndex-- 140 | } 141 | 142 | return true 143 | } 144 | 145 | // matchesSuffixWildcard verifies that 2 keys match word by word. 146 | func (mh MQTTMessageHandlers) matchesKey(storedWords, words []string) bool { 147 | // If the lengths are not the same then surely the key does not match. 148 | if len(storedWords) != len(words) { 149 | return false 150 | } 151 | 152 | // Now we can compare, word by word if the routing keys matches. 153 | for i, word := range words { 154 | // Be careful, if we find '*' then it should match no matter what. 155 | if storedWords[i] != word && storedWords[i] != "*" { 156 | return false 157 | } 158 | } 159 | 160 | return true 161 | } 162 | 163 | func (mh MQTTMessageHandlers) FindFunc(routingKey string) MQTTMessageHandlerFunc { 164 | // We first check for a direct match 165 | if fn, found := mh[routingKey]; found { 166 | return fn 167 | } 168 | 169 | // Split the routing key into individual words. 170 | words := strings.Split(routingKey, ".") 171 | 172 | // Check if any of the registered keys match the routing key. 173 | for key, fn := range mh { 174 | // Split the registered key into individual words. 175 | storedWords := strings.Split(key, ".") 176 | 177 | //nolint: gocritic,nestif // We need this if-else block 178 | if storedWords[0] == "#" { 179 | if !mh.matchesPrefixWildcard(storedWords, words) { 180 | continue 181 | } 182 | } else if storedWords[len(storedWords)-1] == "#" { 183 | if !mh.matchesSuffixWildcard(storedWords, words) { 184 | continue 185 | } 186 | } else { 187 | if !mh.matchesKey(storedWords, words) { 188 | continue 189 | } 190 | } 191 | 192 | return fn 193 | } 194 | 195 | // No matching keys were found. 196 | return nil 197 | } 198 | 199 | // MessageConsumer holds all the information needed to consume messages. 200 | type MessageConsumer struct { 201 | // Queue defines the queue from which we want to consume messages. 202 | Queue string 203 | 204 | // Name is a unique identifier of the consumer. Should be as explicit as possible. 205 | Name string 206 | 207 | // PrefetchSize defines the max size of messages that are allowed to be processed at the same time. 208 | // This property is dropped if AutoAck is set to true. 209 | PrefetchSize int 210 | 211 | // PrefetchCount defines the max number of messages that are allowed to be processed at the same time. 212 | // This property is dropped if AutoAck is set to true. 213 | PrefetchCount int 214 | 215 | // AutoAck defines whether a message is directly acknowledged or not when being consumed. 216 | AutoAck bool 217 | 218 | // ConcurrentProcess will make MQTTMessageHandlers run concurrently for faster consumption, if set to true. 219 | ConcurrentProcess bool 220 | 221 | // Handlers is the list of defined handlers. 222 | Handlers MQTTMessageHandlers 223 | } 224 | 225 | // HashCode returns a unique identifier for the defined consumer. 226 | func (c MessageConsumer) HashCode() string { 227 | return fmt.Sprintf("%s-%s", c.Queue, c.Name) 228 | } 229 | -------------------------------------------------------------------------------- /consumer_test.go: -------------------------------------------------------------------------------- 1 | package gorabbit_test 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | "github.com/KardinalAI/gorabbit" 10 | ) 11 | 12 | func TestMQTTMessageHandlers_Validate(t *testing.T) { 13 | tests := []struct { 14 | handlers gorabbit.MQTTMessageHandlers 15 | expectedError error 16 | }{ 17 | { 18 | handlers: gorabbit.MQTTMessageHandlers{ 19 | "event.user.#": func(_ []byte) error { return nil }, 20 | "event.email.*.generated": func(_ []byte) error { return nil }, 21 | "event.*.space.boom": func(_ []byte) error { return nil }, 22 | "*.toto.order.passed": func(_ []byte) error { return nil }, 23 | "#.toto": func(_ []byte) error { return nil }, 24 | }, 25 | expectedError: nil, 26 | }, 27 | { 28 | handlers: gorabbit.MQTTMessageHandlers{ 29 | "": func(_ []byte) error { return nil }, 30 | }, 31 | expectedError: errors.New("a routing key cannot be empty"), 32 | }, 33 | { 34 | handlers: gorabbit.MQTTMessageHandlers{ 35 | " ": func(_ []byte) error { return nil }, 36 | }, 37 | expectedError: errors.New("a routing key cannot contain spaces"), 38 | }, 39 | { 40 | handlers: gorabbit.MQTTMessageHandlers{ 41 | "#": func(_ []byte) error { return nil }, 42 | }, 43 | expectedError: errors.New("a routing key cannot be the wildcard '#'"), 44 | }, 45 | { 46 | handlers: gorabbit.MQTTMessageHandlers{ 47 | "toto.#.titi": func(_ []byte) error { return nil }, 48 | }, 49 | expectedError: errors.New("the wildcard '#' in the routing key 'toto.#.titi' is not allowed"), 50 | }, 51 | { 52 | handlers: gorabbit.MQTTMessageHandlers{ 53 | "toto titi": func(_ []byte) error { return nil }, 54 | }, 55 | expectedError: errors.New("a routing key cannot contain spaces"), 56 | }, 57 | { 58 | handlers: gorabbit.MQTTMessageHandlers{ 59 | "toto..titi": func(_ []byte) error { return nil }, 60 | }, 61 | expectedError: errors.New("the routing key 'toto..titi' is not properly formatted"), 62 | }, 63 | { 64 | handlers: gorabbit.MQTTMessageHandlers{ 65 | ".toto.titi": func(_ []byte) error { return nil }, 66 | }, 67 | expectedError: errors.New("the routing key '.toto.titi' is not properly formatted"), 68 | }, 69 | { 70 | handlers: gorabbit.MQTTMessageHandlers{ 71 | "toto.titi.": func(_ []byte) error { return nil }, 72 | }, 73 | expectedError: errors.New("the routing key 'toto.titi.' is not properly formatted"), 74 | }, 75 | } 76 | 77 | for _, test := range tests { 78 | err := test.handlers.Validate() 79 | 80 | assert.Equal(t, test.expectedError, err) 81 | } 82 | } 83 | 84 | func TestMQTTMessageHandlers_FindFunc(t *testing.T) { 85 | handlers := gorabbit.MQTTMessageHandlers{ 86 | "event.user.#": func(_ []byte) error { return nil }, 87 | "event.email.*.generated": func(_ []byte) error { return nil }, 88 | "event.*.space.boom": func(_ []byte) error { return nil }, 89 | "*.toto.order.passed": func(_ []byte) error { return nil }, 90 | "#.toto": func(_ []byte) error { return nil }, 91 | } 92 | 93 | tests := []struct { 94 | input string 95 | shouldMatch bool 96 | }{ 97 | { 98 | input: "event.user.plan.generated", 99 | shouldMatch: true, 100 | }, 101 | { 102 | input: "event.user.password.generated.before.awakening.the.titan", 103 | shouldMatch: true, 104 | }, 105 | { 106 | input: "event.email.subject.generated", 107 | shouldMatch: true, 108 | }, 109 | { 110 | input: "event.email.toto.generated", 111 | shouldMatch: true, 112 | }, 113 | { 114 | input: "event.email.titi.generated", 115 | shouldMatch: true, 116 | }, 117 | { 118 | input: "event.email.order.created", 119 | shouldMatch: false, 120 | }, 121 | { 122 | input: "event.toto.space.boom", 123 | shouldMatch: true, 124 | }, 125 | { 126 | input: "event.toto.space.not_boom", 127 | shouldMatch: false, 128 | }, 129 | { 130 | input: "command.toto.order.passed", 131 | shouldMatch: true, 132 | }, 133 | { 134 | input: "command.toto.order.passed.please", 135 | shouldMatch: false, 136 | }, 137 | { 138 | input: "event.toto", 139 | shouldMatch: true, 140 | }, 141 | { 142 | input: "event.space.space.toto", 143 | shouldMatch: true, 144 | }, 145 | { 146 | input: "event.toto.space", 147 | shouldMatch: false, 148 | }, 149 | } 150 | 151 | for _, test := range tests { 152 | fn := handlers.FindFunc(test.input) 153 | 154 | if test.shouldMatch { 155 | assert.NotNil(t, fn) 156 | } else { 157 | assert.Nil(t, fn) 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/KardinalAI/gorabbit 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/Netflix/go-env v0.0.0-20220526054621-78278af1949d 7 | github.com/google/uuid v1.6.0 8 | github.com/rabbitmq/amqp091-go v1.10.0 9 | github.com/sirupsen/logrus v1.9.3 10 | github.com/stretchr/testify v1.9.0 11 | github.com/testcontainers/testcontainers-go v0.30.0 12 | github.com/testcontainers/testcontainers-go/modules/rabbitmq v0.30.0 13 | ) 14 | 15 | require ( 16 | dario.cat/mergo v1.0.0 // indirect 17 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect 18 | github.com/Microsoft/go-winio v0.6.1 // indirect 19 | github.com/Microsoft/hcsshim v0.11.4 // indirect 20 | github.com/cenkalti/backoff/v4 v4.2.1 // indirect 21 | github.com/containerd/containerd v1.7.12 // indirect 22 | github.com/containerd/log v0.1.0 // indirect 23 | github.com/cpuguy83/dockercfg v0.3.1 // indirect 24 | github.com/davecgh/go-spew v1.1.1 // indirect 25 | github.com/distribution/reference v0.5.0 // indirect 26 | github.com/docker/docker v25.0.5+incompatible // indirect 27 | github.com/docker/go-connections v0.5.0 // indirect 28 | github.com/docker/go-units v0.5.0 // indirect 29 | github.com/felixge/httpsnoop v1.0.4 // indirect 30 | github.com/go-logr/logr v1.4.1 // indirect 31 | github.com/go-logr/stdr v1.2.2 // indirect 32 | github.com/go-ole/go-ole v1.2.6 // indirect 33 | github.com/gogo/protobuf v1.3.2 // indirect 34 | github.com/golang/protobuf v1.5.3 // indirect 35 | github.com/klauspost/compress v1.16.0 // indirect 36 | github.com/kr/text v0.2.0 // indirect 37 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect 38 | github.com/magiconair/properties v1.8.7 // indirect 39 | github.com/moby/patternmatcher v0.6.0 // indirect 40 | github.com/moby/sys/sequential v0.5.0 // indirect 41 | github.com/moby/sys/user v0.1.0 // indirect 42 | github.com/moby/term v0.5.0 // indirect 43 | github.com/morikuni/aec v1.0.0 // indirect 44 | github.com/opencontainers/go-digest v1.0.0 // indirect 45 | github.com/opencontainers/image-spec v1.1.0 // indirect 46 | github.com/pkg/errors v0.9.1 // indirect 47 | github.com/pmezard/go-difflib v1.0.0 // indirect 48 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect 49 | github.com/shirou/gopsutil/v3 v3.23.12 // indirect 50 | github.com/shoenig/go-m1cpu v0.1.6 // indirect 51 | github.com/tklauser/go-sysconf v0.3.12 // indirect 52 | github.com/tklauser/numcpus v0.6.1 // indirect 53 | github.com/yusufpapurcu/wmi v1.2.3 // indirect 54 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect 55 | go.opentelemetry.io/otel v1.24.0 // indirect 56 | go.opentelemetry.io/otel/metric v1.24.0 // indirect 57 | go.opentelemetry.io/otel/trace v1.24.0 // indirect 58 | golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea // indirect 59 | golang.org/x/mod v0.16.0 // indirect 60 | golang.org/x/sys v0.19.0 // indirect 61 | golang.org/x/tools v0.13.0 // indirect 62 | google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect 63 | google.golang.org/grpc v1.58.3 // indirect 64 | google.golang.org/protobuf v1.33.0 // indirect 65 | gopkg.in/yaml.v3 v3.0.1 // indirect 66 | ) 67 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= 2 | dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= 3 | github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= 4 | github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= 5 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= 6 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= 7 | github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= 8 | github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= 9 | github.com/Microsoft/hcsshim v0.11.4 h1:68vKo2VN8DE9AdN4tnkWnmdhqdbpUFM8OF3Airm7fz8= 10 | github.com/Microsoft/hcsshim v0.11.4/go.mod h1:smjE4dvqPX9Zldna+t5FG3rnoHhaB7QYxPRqGcpAD9w= 11 | github.com/Netflix/go-env v0.0.0-20220526054621-78278af1949d h1:wvStE9wLpws31NiWUx+38wny1msZ/tm+eL5xmm4Y7So= 12 | github.com/Netflix/go-env v0.0.0-20220526054621-78278af1949d/go.mod h1:9XMFaCeRyW7fC9XJOWQ+NdAv8VLG7ys7l3x4ozEGLUQ= 13 | github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= 14 | github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 15 | github.com/containerd/containerd v1.7.12 h1:+KQsnv4VnzyxWcfO9mlxxELaoztsDEjOuCMPAuPqgU0= 16 | github.com/containerd/containerd v1.7.12/go.mod h1:/5OMpE1p0ylxtEUGY8kuCYkDRzJm9NO1TFMWjUpdevk= 17 | github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= 18 | github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= 19 | github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= 20 | github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= 21 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 22 | github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= 23 | github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 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/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= 28 | github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= 29 | github.com/docker/docker v25.0.5+incompatible h1:UmQydMduGkrD5nQde1mecF/YnSbTOaPeFIeP5C4W+DE= 30 | github.com/docker/docker v25.0.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 31 | github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= 32 | github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= 33 | github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= 34 | github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 35 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 36 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 37 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 38 | github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= 39 | github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 40 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 41 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 42 | github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= 43 | github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= 44 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 45 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 46 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 47 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 48 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 49 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 50 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 51 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 52 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 53 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 54 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 55 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 56 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= 57 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= 58 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 59 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 60 | github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4= 61 | github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= 62 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 63 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 64 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 65 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 66 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= 67 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= 68 | github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= 69 | github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 70 | github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= 71 | github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= 72 | github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= 73 | github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= 74 | github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= 75 | github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= 76 | github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= 77 | github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= 78 | github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= 79 | github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= 80 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 81 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 82 | github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= 83 | github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= 84 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 85 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 86 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 87 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 88 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= 89 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= 90 | github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw= 91 | github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= 92 | github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= 93 | github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= 94 | github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= 95 | github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= 96 | github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= 97 | github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= 98 | github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= 99 | github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= 100 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 101 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 102 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 103 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 104 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 105 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 106 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 107 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 108 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 109 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 110 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 111 | github.com/testcontainers/testcontainers-go v0.30.0 h1:jmn/XS22q4YRrcMwWg0pAwlClzs/abopbsBzrepyc4E= 112 | github.com/testcontainers/testcontainers-go v0.30.0/go.mod h1:K+kHNGiM5zjklKjgTtcrEetF3uhWbMUyqAQoyoh8Pf0= 113 | github.com/testcontainers/testcontainers-go/modules/rabbitmq v0.30.0 h1:FtkqA628qBpEmPj+yTaCgWzpR4ERT1A4oad8nvhDYgQ= 114 | github.com/testcontainers/testcontainers-go/modules/rabbitmq v0.30.0/go.mod h1:JC5EnBLOGC5eEL0Vcf3bwvUAKixuoZsJ4K3g0ioZ+WU= 115 | github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= 116 | github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= 117 | github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= 118 | github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= 119 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 120 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 121 | github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= 122 | github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= 123 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= 124 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= 125 | go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= 126 | go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= 127 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= 128 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= 129 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= 130 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= 131 | go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= 132 | go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= 133 | go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o= 134 | go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= 135 | go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= 136 | go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= 137 | go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= 138 | go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= 139 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 140 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 141 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 142 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 143 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 144 | golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea h1:vLCWI/yYrdEHyN2JzIzPO3aaQJHQdp89IZBA/+azVC4= 145 | golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= 146 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 147 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 148 | golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= 149 | golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 150 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 151 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 152 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 153 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 154 | golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= 155 | golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= 156 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 157 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 158 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 159 | golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= 160 | golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 161 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 162 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 163 | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 164 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 165 | golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 166 | golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 167 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 168 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 169 | golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 170 | golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 171 | golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= 172 | golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 173 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 174 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 175 | golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= 176 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 177 | golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= 178 | golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 179 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 180 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 181 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 182 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 183 | golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= 184 | golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= 185 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 186 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 187 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 188 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 189 | google.golang.org/genproto v0.0.0-20230711160842-782d3b101e98 h1:Z0hjGZePRE0ZBWotvtrwxFNrNE9CUAGtplaDK5NNI/g= 190 | google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 h1:FmF5cCW94Ij59cfpoLiwTgodWmm60eEV0CjlsVg2fuw= 191 | google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98/go.mod h1:rsr7RhLuwsDKL7RmgDDCUc6yaGr1iqceVb5Wv6f6YvQ= 192 | google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 h1:bVf09lpb+OJbByTj913DRJioFFAjf/ZGxEz7MajTp2U= 193 | google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM= 194 | google.golang.org/grpc v1.58.3 h1:BjnpXut1btbtgN/6sp+brB2Kbm2LjNXnidYujAVbSoQ= 195 | google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= 196 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 197 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 198 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 199 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 200 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 201 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 202 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 203 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 204 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 205 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 206 | gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY= 207 | gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= 208 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package gorabbit 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/sirupsen/logrus" 7 | ) 8 | 9 | type logField struct { 10 | Key string 11 | Value interface{} 12 | } 13 | 14 | // logger is the interface that defines log methods. 15 | type logger interface { 16 | Error(error, string, ...logField) 17 | 18 | Warn(string, ...logField) 19 | 20 | Info(string, ...logField) 21 | 22 | Debug(string, ...logField) 23 | } 24 | 25 | // stdLogger logs to stdout using logrus (https://github.com/sirupsen/logrus). 26 | type stdLogger struct { 27 | logger *logrus.Logger 28 | identifier string 29 | logFields map[string]interface{} 30 | } 31 | 32 | func newStdLogger() logger { 33 | return &stdLogger{ 34 | logger: newLogrus(), 35 | identifier: libraryName, 36 | logFields: nil, 37 | } 38 | } 39 | 40 | func (l stdLogger) getExtraFields(fields []logField) map[string]interface{} { 41 | extraFields := make(map[string]interface{}) 42 | 43 | for k, field := range l.logFields { 44 | extraFields[k] = field 45 | } 46 | 47 | for _, extraField := range fields { 48 | extraFields[extraField.Key] = extraField.Value 49 | } 50 | 51 | return extraFields 52 | } 53 | 54 | func (l stdLogger) Error(err error, s string, fields ...logField) { 55 | log := l.logger.WithField("library", l.identifier) 56 | 57 | extraFields := l.getExtraFields(fields) 58 | 59 | log.WithFields(extraFields).WithError(err).Error(s) 60 | } 61 | 62 | func (l stdLogger) Warn(s string, fields ...logField) { 63 | log := l.logger.WithField("library", l.identifier) 64 | 65 | extraFields := l.getExtraFields(fields) 66 | 67 | log.WithFields(extraFields).Warn(s) 68 | } 69 | 70 | func (l stdLogger) Info(s string, fields ...logField) { 71 | log := l.logger.WithField("library", l.identifier) 72 | 73 | extraFields := l.getExtraFields(fields) 74 | 75 | log.WithFields(extraFields).Info(s) 76 | } 77 | 78 | func (l stdLogger) Debug(s string, fields ...logField) { 79 | log := l.logger.WithField("library", l.identifier) 80 | 81 | extraFields := l.getExtraFields(fields) 82 | 83 | log.WithFields(extraFields).Debug(s) 84 | } 85 | 86 | // noLogger does not log at all, this is the default. 87 | type noLogger struct{} 88 | 89 | func (l noLogger) Error(_ error, _ string, _ ...logField) {} 90 | 91 | func (l noLogger) Warn(_ string, _ ...logField) {} 92 | 93 | func (l noLogger) Info(_ string, _ ...logField) {} 94 | 95 | func (l noLogger) Debug(_ string, _ ...logField) {} 96 | 97 | func newLogrus() *logrus.Logger { 98 | log := &logrus.Logger{ 99 | Out: os.Stdout, 100 | Formatter: &logrus.JSONFormatter{ 101 | DisableTimestamp: true, 102 | }, 103 | Level: logrus.DebugLevel, 104 | } 105 | 106 | logLevel := os.Getenv("LOG_LEVEL") 107 | if logLevel != "" { 108 | lvl, err := logrus.ParseLevel(logLevel) 109 | if err == nil { 110 | log.Level = lvl 111 | } 112 | } 113 | 114 | return log 115 | } 116 | 117 | func inheritLogger(parent logger, logFields map[string]interface{}) logger { 118 | switch v := parent.(type) { 119 | case *stdLogger: 120 | return &stdLogger{ 121 | logger: v.logger, 122 | identifier: libraryName, 123 | logFields: logFields, 124 | } 125 | default: 126 | return parent 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /manager.go: -------------------------------------------------------------------------------- 1 | package gorabbit 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "os" 8 | "time" 9 | 10 | "github.com/google/uuid" 11 | amqp "github.com/rabbitmq/amqp091-go" 12 | ) 13 | 14 | // MQTTManager is a simple MQTT interface that offers basic management operations such as: 15 | // - Creation of queue, exchange and bindings 16 | // - Deletion of queues and exchanges 17 | // - Purge of queues 18 | // - Queue evaluation (existence and number of messages) 19 | type MQTTManager interface { 20 | // Disconnect launches the disconnection process. 21 | // This operation disables to manager permanently. 22 | Disconnect() error 23 | 24 | // CreateQueue will create a new queue from QueueConfig. 25 | CreateQueue(config QueueConfig) error 26 | 27 | // CreateExchange will create a new exchange from ExchangeConfig. 28 | CreateExchange(config ExchangeConfig) error 29 | 30 | // BindExchangeToQueueViaRoutingKey will bind an exchange to a queue via a given routingKey. 31 | // Returns an error if the connection to the RabbitMQ server is down or if the exchange or queue does not exist. 32 | BindExchangeToQueueViaRoutingKey(exchange, queue, routingKey string) error 33 | 34 | // GetNumberOfMessages retrieves the number of messages currently sitting in a given queue. 35 | // Returns an error if the connection to the RabbitMQ server is down or the queue does not exist. 36 | GetNumberOfMessages(queue string) (int, error) 37 | 38 | // PushMessageToExchange pushes a message to a given exchange with a given routing key. 39 | // Returns an error if the connection to the RabbitMQ server is down or the exchange does not exist. 40 | PushMessageToExchange(exchange, routingKey string, payload interface{}) error 41 | 42 | // PopMessageFromQueue retrieves the first message of a queue. The message can then be auto-acknowledged or not. 43 | // Returns an error if the connection to the RabbitMQ server is down or the queue does not exist or is empty. 44 | PopMessageFromQueue(queue string, autoAck bool) (*amqp.Delivery, error) 45 | 46 | // PurgeQueue will empty a queue of all its current messages. 47 | // Returns an error if the connection to the RabbitMQ server is down or the queue does not exist. 48 | PurgeQueue(queue string) error 49 | 50 | // DeleteQueue permanently deletes an existing queue. 51 | // Returns an error if the connection to the RabbitMQ server is down or the queue does not exist. 52 | DeleteQueue(queue string) error 53 | 54 | // DeleteExchange permanently deletes an existing exchange. 55 | // Returns an error if the connection to the RabbitMQ server is down or the exchange does not exist. 56 | DeleteExchange(exchange string) error 57 | 58 | // SetupFromDefinitions loads a definitions.json file and automatically sets up exchanges, queues and bindings. 59 | SetupFromDefinitions(path string) error 60 | 61 | // GetHost returns the host used to initialize the manager. 62 | GetHost() string 63 | 64 | // GetPort returns the port used to initialize the manager. 65 | GetPort() uint 66 | 67 | // GetUsername returns the username used to initialize the manager. 68 | GetUsername() string 69 | 70 | // GetVhost returns the vhost used to initialize the manager. 71 | GetVhost() string 72 | 73 | // IsDisabled returns whether the manager is disabled or not. 74 | IsDisabled() bool 75 | } 76 | 77 | type mqttManager struct { 78 | // Host is the RabbitMQ server host name. 79 | Host string 80 | 81 | // Port is the RabbitMQ server port number. 82 | Port uint 83 | 84 | // Username is the RabbitMQ server allowed username. 85 | Username string 86 | 87 | // Password is the RabbitMQ server allowed password. 88 | Password string 89 | 90 | // Vhost is used for CloudAMQP connections to set the specific vhost. 91 | Vhost string 92 | 93 | // logger defines the logger used, depending on the mode set. 94 | logger logger 95 | 96 | // disabled completely disables the manager if true. 97 | disabled bool 98 | 99 | // connection holds the single connection to the RabbitMQ server. 100 | connection *amqp.Connection 101 | 102 | // channel holds the single channel from the connection. 103 | channel *amqp.Channel 104 | 105 | // marshaller holds the marshaller used to encode messages. 106 | marshaller Marshaller 107 | } 108 | 109 | // NewManager will instantiate a new MQTTManager. 110 | // If options is set to nil, the DefaultManagerOptions will be used. 111 | func NewManager(options *ManagerOptions) (MQTTManager, error) { 112 | // If no options is passed, we use the DefaultManagerOptions. 113 | if options == nil { 114 | options = DefaultManagerOptions() 115 | } 116 | 117 | return newManagerFromOptions(options) 118 | } 119 | 120 | // NewManagerFromEnv will instantiate a new MQTTManager from environment variables. 121 | func NewManagerFromEnv() (MQTTManager, error) { 122 | options := NewManagerOptionsFromEnv() 123 | 124 | return newManagerFromOptions(options) 125 | } 126 | 127 | func newManagerFromOptions(options *ManagerOptions) (MQTTManager, error) { 128 | manager := &mqttManager{ 129 | Host: options.Host, 130 | Port: options.Port, 131 | Username: options.Username, 132 | Password: options.Password, 133 | Vhost: options.Vhost, 134 | logger: &noLogger{}, 135 | } 136 | 137 | // We check if the disabled flag is present, which will completely disable the MQTTManager. 138 | if disabledOverride := os.Getenv("GORABBIT_DISABLED"); disabledOverride != "" { 139 | switch disabledOverride { 140 | case "1", "true": 141 | manager.disabled = true 142 | 143 | return manager, nil 144 | } 145 | } 146 | 147 | // We check if the mode was overwritten with the environment variable "GORABBIT_MODE". 148 | if modeOverride := os.Getenv("GORABBIT_MODE"); isValidMode(modeOverride) { 149 | // We override the mode only if it is valid 150 | options.Mode = modeOverride 151 | } 152 | 153 | if options.Mode == Debug { 154 | // If the mode is Debug, we want to actually log important events. 155 | manager.logger = newStdLogger() 156 | } 157 | 158 | protocol := defaultProtocol 159 | 160 | if options.UseTLS { 161 | protocol = securedProtocol 162 | } 163 | 164 | if options.Marshaller == nil { 165 | options.Marshaller = defaultMarshaller 166 | } 167 | manager.marshaller = options.Marshaller 168 | 169 | dialURL := fmt.Sprintf("%s://%s:%s@%s:%d/%s", protocol, manager.Username, manager.Password, manager.Host, manager.Port, manager.Vhost) 170 | 171 | var err error 172 | 173 | manager.connection, err = amqp.Dial(dialURL) 174 | if err != nil { 175 | return manager, err 176 | } 177 | 178 | manager.channel, err = manager.connection.Channel() 179 | if err != nil { 180 | return manager, err 181 | } 182 | 183 | return manager, nil 184 | } 185 | 186 | func (manager *mqttManager) Disconnect() error { 187 | // Manager is disabled, so we do nothing and return no error. 188 | if manager.disabled { 189 | return nil 190 | } 191 | 192 | // We close the manager's channel only if it is opened. 193 | if manager.channel != nil && !manager.channel.IsClosed() { 194 | err := manager.channel.Close() 195 | if err != nil { 196 | return err 197 | } 198 | } 199 | 200 | // We close the manager's connection only if it is opened. 201 | if manager.connection != nil && !manager.connection.IsClosed() { 202 | return manager.connection.Close() 203 | } 204 | 205 | return nil 206 | } 207 | 208 | func (manager *mqttManager) CreateQueue(config QueueConfig) error { 209 | // Manager is disabled, so we do nothing and return no error. 210 | if manager.disabled { 211 | return nil 212 | } 213 | 214 | // If the manager is not ready, we return its error. 215 | if ready, err := manager.ready(); !ready { 216 | return err 217 | } 218 | 219 | // We declare the queue via the channel. 220 | _, err := manager.channel.QueueDeclare( 221 | config.Name, // name 222 | config.Durable, // durable 223 | config.AutoDelete, // delete when unused 224 | config.Exclusive, // exclusive 225 | false, // no-wait 226 | config.Args, 227 | ) 228 | 229 | if err != nil { 230 | return err 231 | } 232 | 233 | // If bindings are also declared, we create the bindings too. 234 | if config.Bindings != nil { 235 | for _, binding := range config.Bindings { 236 | err = manager.BindExchangeToQueueViaRoutingKey(binding.Exchange, config.Name, binding.RoutingKey) 237 | 238 | if err != nil { 239 | return err 240 | } 241 | } 242 | } 243 | 244 | return nil 245 | } 246 | 247 | func (manager *mqttManager) CreateExchange(config ExchangeConfig) error { 248 | // Manager is disabled, so we do nothing and return no error. 249 | if manager.disabled { 250 | return nil 251 | } 252 | 253 | // If the manager is not ready, we return its error. 254 | if ready, err := manager.ready(); !ready { 255 | return err 256 | } 257 | 258 | // We declare the exchange via the channel. 259 | return manager.channel.ExchangeDeclare( 260 | config.Name, // name 261 | config.Type.String(), // type 262 | config.Persisted, // durable 263 | !config.Persisted, // auto-deleted 264 | false, // internal 265 | false, // no-wait 266 | config.Args, // arguments 267 | ) 268 | } 269 | 270 | func (manager *mqttManager) BindExchangeToQueueViaRoutingKey(exchange, queue, routingKey string) error { 271 | // Manager is disabled, so we do nothing and return no error. 272 | if manager.disabled { 273 | return nil 274 | } 275 | 276 | // If the manager is not ready, we return its error. 277 | if ready, err := manager.ready(); !ready { 278 | return err 279 | } 280 | 281 | // We bind the queue to a given exchange and routing key via the channel. 282 | return manager.channel.QueueBind( 283 | queue, 284 | routingKey, 285 | exchange, 286 | false, 287 | nil, 288 | ) 289 | } 290 | 291 | func (manager *mqttManager) GetNumberOfMessages(queue string) (int, error) { 292 | // Manager is disabled, so we do nothing and return no error. 293 | if manager.disabled { 294 | return -1, nil 295 | } 296 | 297 | // If the manager is not ready, we return its error. 298 | if ready, err := manager.ready(); !ready { 299 | return -1, err 300 | } 301 | 302 | // We passively declare the queue via the channel, this will return the existing queue or an error if it doesn't exist. 303 | q, err := manager.channel.QueueDeclarePassive( 304 | queue, 305 | false, 306 | false, 307 | false, 308 | false, 309 | nil, 310 | ) 311 | 312 | if err != nil { 313 | return -1, err 314 | } 315 | 316 | return q.Messages, nil 317 | } 318 | 319 | func (manager *mqttManager) PushMessageToExchange(exchange, routingKey string, payload interface{}) error { 320 | // Manager is disabled, so we do nothing and return no error. 321 | if manager.disabled { 322 | return nil 323 | } 324 | 325 | // If the manager is not ready, we return its error. 326 | if ready, err := manager.ready(); !ready { 327 | return err 328 | } 329 | 330 | // We convert the payload to a []byte. 331 | payloadBytes, err := manager.marshaller.Marshal(payload) 332 | if err != nil { 333 | return err 334 | } 335 | 336 | // We build the amqp.Publishing object. 337 | publishing := amqp.Publishing{ 338 | ContentType: manager.marshaller.ContentType(), 339 | Body: payloadBytes, 340 | Type: routingKey, 341 | Priority: PriorityMedium.Uint8(), 342 | DeliveryMode: Transient.Uint8(), 343 | MessageId: uuid.NewString(), 344 | Timestamp: time.Now(), 345 | } 346 | 347 | // We push the message via the channel. 348 | return manager.channel.PublishWithContext(context.TODO(), exchange, routingKey, false, false, publishing) 349 | } 350 | 351 | func (manager *mqttManager) PopMessageFromQueue(queue string, autoAck bool) (*amqp.Delivery, error) { 352 | // Manager is disabled, so we do nothing and return no error. 353 | if manager.disabled { 354 | //nolint: nilnil // We must return 355 | return nil, nil 356 | } 357 | 358 | // If the manager is not ready, we return its error. 359 | if ready, err := manager.ready(); !ready { 360 | return nil, err 361 | } 362 | 363 | // We get the message via the channel. 364 | m, ok, err := manager.channel.Get(queue, autoAck) 365 | 366 | if err != nil { 367 | return nil, err 368 | } 369 | 370 | // If the queue is empty. 371 | if !ok { 372 | return nil, errEmptyQueue 373 | } 374 | 375 | return &m, nil 376 | } 377 | 378 | func (manager *mqttManager) PurgeQueue(queue string) error { 379 | // Manager is disabled, so we do nothing and return no error. 380 | if manager.disabled { 381 | return nil 382 | } 383 | 384 | // If the manager is not ready, we return its error. 385 | if ready, err := manager.ready(); !ready { 386 | return err 387 | } 388 | 389 | // We purge the queue via the channel. 390 | _, err := manager.channel.QueuePurge(queue, false) 391 | 392 | if err != nil { 393 | return err 394 | } 395 | 396 | return nil 397 | } 398 | 399 | func (manager *mqttManager) DeleteQueue(queue string) error { 400 | // Manager is disabled, so we do nothing and return no error. 401 | if manager.disabled { 402 | return nil 403 | } 404 | 405 | // If the manager is not ready, we return its error. 406 | if ready, err := manager.ready(); !ready { 407 | return err 408 | } 409 | 410 | // We delete the queue via the channel. 411 | _, err := manager.channel.QueueDelete(queue, false, false, false) 412 | 413 | if err != nil { 414 | return err 415 | } 416 | 417 | return nil 418 | } 419 | 420 | func (manager *mqttManager) DeleteExchange(exchange string) error { 421 | // Manager is disabled, so we do nothing and return no error. 422 | if manager.disabled { 423 | return nil 424 | } 425 | 426 | // If the manager is not ready, we return its error. 427 | if ready, err := manager.ready(); !ready { 428 | return err 429 | } 430 | 431 | // We delete the exchange via the channel. 432 | return manager.channel.ExchangeDelete(exchange, false, false) 433 | } 434 | 435 | func (manager *mqttManager) SetupFromDefinitions(path string) error { 436 | // Manager is disabled, so we do nothing and return no error. 437 | if manager.disabled { 438 | return nil 439 | } 440 | 441 | // If the manager is not ready, we return its error. 442 | if ready, err := manager.ready(); !ready { 443 | return err 444 | } 445 | 446 | // We read the definitions.json file. 447 | definitions, err := os.ReadFile(path) 448 | if err != nil { 449 | return err 450 | } 451 | 452 | def := new(SchemaDefinitions) 453 | 454 | // We parse the definitions.json file into the corresponding struct. 455 | err = json.Unmarshal(definitions, def) 456 | if err != nil { 457 | return err 458 | } 459 | 460 | for _, queue := range def.Queues { 461 | // We create the queue. 462 | err = manager.CreateQueue(QueueConfig{ 463 | Name: queue.Name, 464 | Durable: queue.Durable, 465 | Exclusive: false, 466 | }) 467 | 468 | if err != nil { 469 | return err 470 | } 471 | } 472 | 473 | for _, exchange := range def.Exchanges { 474 | // We create the exchange. 475 | err = manager.CreateExchange(ExchangeConfig{ 476 | Name: exchange.Name, 477 | Type: ExchangeType(exchange.Type), 478 | Persisted: exchange.Durable, 479 | }) 480 | 481 | if err != nil { 482 | return err 483 | } 484 | } 485 | 486 | for _, binding := range def.Bindings { 487 | // We bind the given exchange to the given queue via the given routing key. 488 | err = manager.BindExchangeToQueueViaRoutingKey(binding.Source, binding.Destination, binding.RoutingKey) 489 | 490 | if err != nil { 491 | return err 492 | } 493 | } 494 | 495 | return nil 496 | } 497 | 498 | func (manager *mqttManager) checkChannel() error { 499 | var err error 500 | 501 | // If the connection is nil or closed, we must request a new channel. 502 | if manager.channel == nil || manager.channel.IsClosed() { 503 | manager.channel, err = manager.connection.Channel() 504 | } 505 | 506 | return err 507 | } 508 | 509 | func (manager *mqttManager) ready() (bool, error) { 510 | // Manager is disabled, so we do nothing and return no error. 511 | if manager.disabled { 512 | return true, nil 513 | } 514 | 515 | // If the connection is nil or closed, we return an error because the manager is not ready. 516 | if manager.connection == nil || manager.connection.IsClosed() { 517 | return false, errConnectionClosed 518 | } 519 | 520 | // We check the channel as it might have been closed, and we need to request a new one. 521 | if err := manager.checkChannel(); err != nil { 522 | return false, err 523 | } 524 | 525 | // If the channel is still nil or closed, we return an error because the manager is not ready. 526 | if manager.channel == nil || manager.channel.IsClosed() { 527 | return false, errChannelClosed 528 | } 529 | 530 | return true, nil 531 | } 532 | 533 | func (manager *mqttManager) GetHost() string { 534 | return manager.Host 535 | } 536 | 537 | func (manager *mqttManager) GetPort() uint { 538 | return manager.Port 539 | } 540 | 541 | func (manager *mqttManager) GetUsername() string { 542 | return manager.Username 543 | } 544 | 545 | func (manager *mqttManager) GetVhost() string { 546 | return manager.Vhost 547 | } 548 | 549 | func (manager *mqttManager) IsDisabled() bool { 550 | return manager.disabled 551 | } 552 | -------------------------------------------------------------------------------- /manager_options.go: -------------------------------------------------------------------------------- 1 | package gorabbit 2 | 3 | import "github.com/Netflix/go-env" 4 | 5 | // ManagerOptions holds all necessary properties to launch a successful connection with an MQTTManager. 6 | type ManagerOptions struct { 7 | // Host is the RabbitMQ server host name. 8 | Host string 9 | 10 | // Port is the RabbitMQ server port number. 11 | Port uint 12 | 13 | // Username is the RabbitMQ server allowed username. 14 | Username string 15 | 16 | // Password is the RabbitMQ server allowed password. 17 | Password string 18 | 19 | // Vhost is used for CloudAMQP connections to set the specific vhost. 20 | Vhost string 21 | 22 | // UseTLS defines whether we use amqp or amqps protocol. 23 | UseTLS bool 24 | 25 | // Mode will specify whether logs are enabled or not. 26 | Mode string 27 | 28 | // Marshaller defines the content type used for messages and how they're marshalled (default: JSON). 29 | Marshaller Marshaller 30 | } 31 | 32 | // DefaultManagerOptions will return a ManagerOptions with default values. 33 | func DefaultManagerOptions() *ManagerOptions { 34 | return &ManagerOptions{ 35 | Host: defaultHost, 36 | Port: defaultPort, 37 | Username: defaultUsername, 38 | Password: defaultPassword, 39 | Vhost: defaultVhost, 40 | UseTLS: defaultUseTLS, 41 | Mode: defaultMode, 42 | Marshaller: defaultMarshaller, 43 | } 44 | } 45 | 46 | // NewManagerOptions is the exported builder for a ManagerOptions and will offer setter methods for an easy construction. 47 | // Any non-assigned field will be set to default through DefaultManagerOptions. 48 | func NewManagerOptions() *ManagerOptions { 49 | return DefaultManagerOptions() 50 | } 51 | 52 | // NewManagerOptionsFromEnv will generate a ManagerOptions from environment variables. Empty values will be taken as default 53 | // through the DefaultManagerOptions. 54 | func NewManagerOptionsFromEnv() *ManagerOptions { 55 | defaultOpts := DefaultManagerOptions() 56 | 57 | fromEnv := new(RabbitMQEnvs) 58 | 59 | _, err := env.UnmarshalFromEnviron(fromEnv) 60 | if err != nil { 61 | return defaultOpts 62 | } 63 | 64 | if fromEnv.Host != "" { 65 | defaultOpts.Host = fromEnv.Host 66 | } 67 | 68 | if fromEnv.Port > 0 { 69 | defaultOpts.Port = fromEnv.Port 70 | } 71 | 72 | if fromEnv.Username != "" { 73 | defaultOpts.Username = fromEnv.Username 74 | } 75 | 76 | if fromEnv.Password != "" { 77 | defaultOpts.Password = fromEnv.Password 78 | } 79 | 80 | if fromEnv.Vhost != "" { 81 | defaultOpts.Vhost = fromEnv.Vhost 82 | } 83 | 84 | defaultOpts.UseTLS = fromEnv.UseTLS 85 | 86 | return defaultOpts 87 | } 88 | 89 | // SetHost will assign the host. 90 | func (m *ManagerOptions) SetHost(host string) *ManagerOptions { 91 | m.Host = host 92 | 93 | return m 94 | } 95 | 96 | // SetPort will assign the port. 97 | func (m *ManagerOptions) SetPort(port uint) *ManagerOptions { 98 | m.Port = port 99 | 100 | return m 101 | } 102 | 103 | // SetCredentials will assign the username and password. 104 | func (m *ManagerOptions) SetCredentials(username, password string) *ManagerOptions { 105 | m.Username = username 106 | m.Password = password 107 | 108 | return m 109 | } 110 | 111 | // SetVhost will assign the Vhost. 112 | func (m *ManagerOptions) SetVhost(vhost string) *ManagerOptions { 113 | m.Vhost = vhost 114 | 115 | return m 116 | } 117 | 118 | // SetUseTLS will assign the UseTLS status. 119 | func (m *ManagerOptions) SetUseTLS(use bool) *ManagerOptions { 120 | m.UseTLS = use 121 | 122 | return m 123 | } 124 | 125 | // SetMode will assign the mode if valid. 126 | func (m *ManagerOptions) SetMode(mode string) *ManagerOptions { 127 | if isValidMode(mode) { 128 | m.Mode = mode 129 | } 130 | 131 | return m 132 | } 133 | 134 | // SetMarshaller will assign the Marshaller. 135 | func (m *ManagerOptions) SetMarshaller(marshaller Marshaller) *ManagerOptions { 136 | m.Marshaller = marshaller 137 | 138 | return m 139 | } 140 | -------------------------------------------------------------------------------- /manager_test.go: -------------------------------------------------------------------------------- 1 | package gorabbit_test 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "strconv" 7 | "strings" 8 | "testing" 9 | "time" 10 | 11 | "github.com/KardinalAI/gorabbit" 12 | amqp "github.com/rabbitmq/amqp091-go" 13 | "github.com/stretchr/testify/assert" 14 | "github.com/stretchr/testify/require" 15 | "github.com/stretchr/testify/suite" 16 | "github.com/testcontainers/testcontainers-go" 17 | "github.com/testcontainers/testcontainers-go/modules/rabbitmq" 18 | ) 19 | 20 | type RabbitMQContainer struct { 21 | *rabbitmq.RabbitMQContainer 22 | ContainerHost string 23 | ContainerPort uint 24 | Username string 25 | Password string 26 | } 27 | 28 | func CreateRabbitMQContainer(ctx context.Context) (*RabbitMQContainer, error) { 29 | rContainer, err := rabbitmq.RunContainer(ctx, 30 | testcontainers.WithImage("rabbitmq:3.12.11-management-alpine"), 31 | rabbitmq.WithAdminUsername("guest"), 32 | rabbitmq.WithAdminPassword("guest"), 33 | ) 34 | 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | ep, err := rContainer.AmqpURL(ctx) 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | uri, err := amqp.ParseURI(ep) 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | rabbitMQContainer := &RabbitMQContainer{ 50 | RabbitMQContainer: rContainer, 51 | ContainerHost: uri.Host, 52 | ContainerPort: uint(uri.Port), 53 | Username: uri.Username, 54 | Password: uri.Password, 55 | } 56 | 57 | return rabbitMQContainer, nil 58 | } 59 | 60 | type ManagerTestSuite struct { 61 | suite.Suite 62 | rabbitMQContainer *RabbitMQContainer 63 | ctx context.Context 64 | } 65 | 66 | func (suite *ManagerTestSuite) SetupSuite() { 67 | suite.ctx = context.Background() 68 | 69 | rContainer, err := CreateRabbitMQContainer(suite.ctx) 70 | if err != nil { 71 | log.Fatal(err) 72 | } 73 | 74 | suite.rabbitMQContainer = rContainer 75 | } 76 | 77 | func (suite *ManagerTestSuite) TearDownSuite() { 78 | if err := suite.rabbitMQContainer.Terminate(suite.ctx); err != nil { 79 | log.Fatalf("error terminating RabbitMQ container: %s", err) 80 | } 81 | } 82 | 83 | func (suite *ManagerTestSuite) TestNewManager() { 84 | t := suite.T() 85 | 86 | t.Run("Instantiating new manager with correct parameters", func(t *testing.T) { 87 | managerOpts := gorabbit.NewManagerOptions(). 88 | SetHost(suite.rabbitMQContainer.ContainerHost). 89 | SetPort(suite.rabbitMQContainer.ContainerPort). 90 | SetCredentials(suite.rabbitMQContainer.Username, suite.rabbitMQContainer.Password) 91 | 92 | manager, err := gorabbit.NewManager(managerOpts) 93 | 94 | require.NoError(t, err) 95 | assert.NotNil(t, manager) 96 | 97 | assert.NotEmpty(t, manager.GetHost()) 98 | assert.NotZero(t, manager.GetPort()) 99 | assert.NotEmpty(t, manager.GetUsername()) 100 | 101 | require.NoError(t, manager.Disconnect()) 102 | }) 103 | 104 | t.Run("Instantiating new manager with incorrect credentials", func(t *testing.T) { 105 | managerOpts := gorabbit.NewManagerOptions(). 106 | SetHost(suite.rabbitMQContainer.ContainerHost). 107 | SetPort(suite.rabbitMQContainer.ContainerPort). 108 | SetCredentials("bad", "password") 109 | 110 | manager, err := gorabbit.NewManager(managerOpts) 111 | 112 | require.Error(t, err) 113 | assert.Equal(t, "Exception (403) Reason: \"username or password not allowed\"", err.Error()) 114 | assert.NotNil(t, manager) 115 | 116 | // Running any operation at that point should fail, except for disconnecting 117 | require.NoError(t, manager.Disconnect()) 118 | 119 | err = manager.CreateQueue(gorabbit.QueueConfig{}) 120 | 121 | require.Error(t, err) 122 | assert.Equal(t, "connection is closed", err.Error()) 123 | }) 124 | 125 | t.Run("Instantiating new manager with incorrect host", func(t *testing.T) { 126 | managerOpts := gorabbit.NewManagerOptions(). 127 | SetHost("incorrect_host"). 128 | SetPort(suite.rabbitMQContainer.ContainerPort). 129 | SetCredentials(suite.rabbitMQContainer.Username, suite.rabbitMQContainer.Password) 130 | 131 | manager, err := gorabbit.NewManager(managerOpts) 132 | 133 | require.Error(t, err) 134 | 135 | hasDialPrefix := strings.HasPrefix(err.Error(), "dial tcp: lookup incorrect_host on") 136 | assert.True(t, hasDialPrefix) 137 | 138 | hasDialSuffix := strings.HasSuffix(err.Error(), "server misbehaving") 139 | assert.True(t, hasDialSuffix) 140 | 141 | assert.NotNil(t, manager) 142 | 143 | // Running any operation at that point should fail, except for disconnecting 144 | require.NoError(t, manager.Disconnect()) 145 | 146 | err = manager.CreateQueue(gorabbit.QueueConfig{}) 147 | 148 | require.Error(t, err) 149 | assert.Equal(t, "connection is closed", err.Error()) 150 | }) 151 | 152 | t.Run("Instantiating new manager with incorrect port", func(t *testing.T) { 153 | managerOpts := gorabbit.NewManagerOptions(). 154 | SetHost(suite.rabbitMQContainer.ContainerHost). 155 | SetPort(uint(123)). 156 | SetCredentials(suite.rabbitMQContainer.Username, suite.rabbitMQContainer.Password) 157 | 158 | manager, err := gorabbit.NewManager(managerOpts) 159 | 160 | require.Error(t, err) 161 | 162 | hasDialPrefix := strings.HasPrefix(err.Error(), "dial tcp") 163 | assert.True(t, hasDialPrefix) 164 | 165 | hasDialSuffix := strings.HasSuffix(err.Error(), "connection refused") 166 | assert.True(t, hasDialSuffix) 167 | 168 | assert.NotNil(t, manager) 169 | 170 | // Running any operation at that point should fail, except for disconnecting 171 | require.NoError(t, manager.Disconnect()) 172 | 173 | err = manager.CreateQueue(gorabbit.QueueConfig{}) 174 | 175 | require.Error(t, err) 176 | assert.Equal(t, "connection is closed", err.Error()) 177 | }) 178 | } 179 | 180 | func (suite *ManagerTestSuite) TestNewFromEnv() { 181 | t := suite.T() 182 | 183 | t.Setenv("RABBITMQ_HOST", suite.rabbitMQContainer.ContainerHost) 184 | t.Setenv("RABBITMQ_PORT", strconv.Itoa(int(suite.rabbitMQContainer.ContainerPort))) 185 | t.Setenv("RABBITMQ_USERNAME", suite.rabbitMQContainer.Username) 186 | t.Setenv("RABBITMQ_PASSWORD", suite.rabbitMQContainer.Password) 187 | 188 | manager, err := gorabbit.NewManagerFromEnv() 189 | 190 | require.NoError(t, err) 191 | assert.NotNil(t, manager) 192 | 193 | require.NoError(t, manager.Disconnect()) 194 | 195 | t.Run("Instantiating a manager with disabled flag", func(t *testing.T) { 196 | t.Setenv("GORABBIT_DISABLED", "true") 197 | 198 | manager, err = gorabbit.NewManagerFromEnv() 199 | 200 | require.NoError(t, err) 201 | assert.NotNil(t, manager) 202 | 203 | assert.True(t, manager.IsDisabled()) 204 | }) 205 | } 206 | 207 | func (suite *ManagerTestSuite) TestCreateQueue() { 208 | t := suite.T() 209 | 210 | t.Setenv("RABBITMQ_HOST", suite.rabbitMQContainer.ContainerHost) 211 | t.Setenv("RABBITMQ_PORT", strconv.Itoa(int(suite.rabbitMQContainer.ContainerPort))) 212 | t.Setenv("RABBITMQ_USERNAME", suite.rabbitMQContainer.Username) 213 | t.Setenv("RABBITMQ_PASSWORD", suite.rabbitMQContainer.Password) 214 | 215 | manager, err := gorabbit.NewManagerFromEnv() 216 | 217 | require.NoError(t, err) 218 | assert.NotNil(t, manager) 219 | 220 | t.Run("Creating queue with valid parameters", func(t *testing.T) { 221 | queueConfig := gorabbit.QueueConfig{ 222 | Name: "test_queue", 223 | Durable: true, 224 | Exclusive: false, 225 | } 226 | 227 | err = manager.CreateQueue(queueConfig) 228 | 229 | require.NoError(t, err) 230 | 231 | err = manager.DeleteQueue(queueConfig.Name) 232 | 233 | require.NoError(t, err) 234 | }) 235 | 236 | t.Run("Creating queue with empty name should work", func(t *testing.T) { 237 | queueConfig := gorabbit.QueueConfig{} 238 | 239 | err = manager.CreateQueue(queueConfig) 240 | 241 | require.NoError(t, err) 242 | 243 | err = manager.DeleteQueue(queueConfig.Name) 244 | 245 | require.NoError(t, err) 246 | }) 247 | 248 | t.Run("Creating queue with bindings but non-existent exchange", func(t *testing.T) { 249 | queueConfig := gorabbit.QueueConfig{ 250 | Name: "test_queue", 251 | Durable: true, 252 | Exclusive: false, 253 | Bindings: []gorabbit.BindingConfig{ 254 | { 255 | RoutingKey: "routing_key", 256 | Exchange: "test_exchange", 257 | }, 258 | }, 259 | } 260 | 261 | err = manager.CreateQueue(queueConfig) 262 | 263 | require.Error(t, err) 264 | assert.Equal(t, "Exception (404) Reason: \"NOT_FOUND - no exchange 'test_exchange' in vhost '/'\"", err.Error()) 265 | }) 266 | 267 | t.Run("Creating queue with bindings and existing exchange", func(t *testing.T) { 268 | exchangeConfig := gorabbit.ExchangeConfig{ 269 | Name: "test_exchange_with_bindings", 270 | Type: "topic", 271 | } 272 | 273 | err = manager.CreateExchange(exchangeConfig) 274 | 275 | require.NoError(t, err) 276 | 277 | queueConfig := gorabbit.QueueConfig{ 278 | Name: "test_queue", 279 | Durable: true, 280 | Exclusive: false, 281 | Bindings: []gorabbit.BindingConfig{ 282 | { 283 | RoutingKey: "routing_key", 284 | Exchange: "test_exchange_with_bindings", 285 | }, 286 | }, 287 | } 288 | 289 | err = manager.CreateQueue(queueConfig) 290 | 291 | require.NoError(t, err) 292 | 293 | err = manager.DeleteExchange(exchangeConfig.Name) 294 | 295 | require.NoError(t, err) 296 | 297 | err = manager.DeleteQueue(queueConfig.Name) 298 | 299 | require.NoError(t, err) 300 | }) 301 | 302 | require.NoError(t, manager.Disconnect()) 303 | } 304 | 305 | func (suite *ManagerTestSuite) TestCreateExchange() { 306 | t := suite.T() 307 | 308 | t.Setenv("RABBITMQ_HOST", suite.rabbitMQContainer.ContainerHost) 309 | t.Setenv("RABBITMQ_PORT", strconv.Itoa(int(suite.rabbitMQContainer.ContainerPort))) 310 | t.Setenv("RABBITMQ_USERNAME", suite.rabbitMQContainer.Username) 311 | t.Setenv("RABBITMQ_PASSWORD", suite.rabbitMQContainer.Password) 312 | 313 | manager, err := gorabbit.NewManagerFromEnv() 314 | 315 | require.NoError(t, err) 316 | assert.NotNil(t, manager) 317 | 318 | t.Run("Creating exchange with valid parameters", func(t *testing.T) { 319 | exchangeConfig := gorabbit.ExchangeConfig{ 320 | Name: "test_exchange", 321 | Type: gorabbit.ExchangeTypeTopic, 322 | } 323 | 324 | err = manager.CreateExchange(exchangeConfig) 325 | 326 | require.NoError(t, err) 327 | 328 | err = manager.DeleteExchange(exchangeConfig.Name) 329 | 330 | require.NoError(t, err) 331 | }) 332 | 333 | t.Run("Creating exchange with empty parameters", func(t *testing.T) { 334 | exchangeConfig := gorabbit.ExchangeConfig{} 335 | 336 | err = manager.CreateExchange(exchangeConfig) 337 | 338 | require.Error(t, err) 339 | assert.Equal(t, "Exception (503) Reason: \"COMMAND_INVALID - invalid exchange type ''\"", err.Error()) 340 | 341 | // By now the manager's connection should be closed 342 | err = manager.CreateExchange(exchangeConfig) 343 | 344 | require.Error(t, err) 345 | assert.Equal(t, "connection is closed", err.Error()) 346 | 347 | manager, err = gorabbit.NewManagerFromEnv() 348 | 349 | require.NoError(t, err) 350 | assert.NotNil(t, manager) 351 | }) 352 | 353 | t.Run("Creating exchange with empty name", func(t *testing.T) { 354 | exchangeConfig := gorabbit.ExchangeConfig{ 355 | Name: "", 356 | Type: gorabbit.ExchangeTypeTopic, 357 | } 358 | 359 | err = manager.CreateExchange(exchangeConfig) 360 | 361 | require.Error(t, err) 362 | assert.Equal(t, "Exception (403) Reason: \"ACCESS_REFUSED - operation not permitted on the default exchange\"", err.Error()) 363 | 364 | manager, err = gorabbit.NewManagerFromEnv() 365 | 366 | require.NoError(t, err) 367 | assert.NotNil(t, manager) 368 | }) 369 | 370 | t.Run("Creating all type of exchanges", func(t *testing.T) { 371 | topicExchange := gorabbit.ExchangeConfig{ 372 | Name: "topic_exchange", 373 | Type: gorabbit.ExchangeTypeTopic, 374 | } 375 | 376 | err = manager.CreateExchange(topicExchange) 377 | 378 | require.NoError(t, err) 379 | 380 | directExchange := gorabbit.ExchangeConfig{ 381 | Name: "direct_exchange", 382 | Type: gorabbit.ExchangeTypeDirect, 383 | } 384 | 385 | err = manager.CreateExchange(directExchange) 386 | 387 | require.NoError(t, err) 388 | 389 | fanoutExchange := gorabbit.ExchangeConfig{ 390 | Name: "fanout_exchange", 391 | Type: gorabbit.ExchangeTypeFanout, 392 | } 393 | 394 | err = manager.CreateExchange(fanoutExchange) 395 | 396 | require.NoError(t, err) 397 | 398 | headersExchange := gorabbit.ExchangeConfig{ 399 | Name: "headers_exchange", 400 | Type: gorabbit.ExchangeTypeHeaders, 401 | } 402 | 403 | err = manager.CreateExchange(headersExchange) 404 | 405 | require.NoError(t, err) 406 | 407 | require.NoError(t, manager.DeleteExchange(topicExchange.Name)) 408 | require.NoError(t, manager.DeleteExchange(directExchange.Name)) 409 | require.NoError(t, manager.DeleteExchange(fanoutExchange.Name)) 410 | require.NoError(t, manager.DeleteExchange(headersExchange.Name)) 411 | }) 412 | 413 | require.NoError(t, manager.Disconnect()) 414 | } 415 | 416 | func (suite *ManagerTestSuite) TestBindExchangeToQueueViaRoutingKey() { 417 | t := suite.T() 418 | 419 | t.Setenv("RABBITMQ_HOST", suite.rabbitMQContainer.ContainerHost) 420 | t.Setenv("RABBITMQ_PORT", strconv.Itoa(int(suite.rabbitMQContainer.ContainerPort))) 421 | t.Setenv("RABBITMQ_USERNAME", suite.rabbitMQContainer.Username) 422 | t.Setenv("RABBITMQ_PASSWORD", suite.rabbitMQContainer.Password) 423 | 424 | manager, err := gorabbit.NewManagerFromEnv() 425 | 426 | require.NoError(t, err) 427 | assert.NotNil(t, manager) 428 | 429 | queueConfig := gorabbit.QueueConfig{ 430 | Name: "test_queue", 431 | Durable: true, 432 | Exclusive: false, 433 | } 434 | 435 | err = manager.CreateQueue(queueConfig) 436 | 437 | require.NoError(t, err) 438 | 439 | exchangeConfig := gorabbit.ExchangeConfig{ 440 | Name: "test_exchange", 441 | Type: "topic", 442 | } 443 | 444 | err = manager.CreateExchange(exchangeConfig) 445 | 446 | require.NoError(t, err) 447 | 448 | t.Run("Binding existing exchange to existing queue via routing key", func(t *testing.T) { 449 | err = manager.BindExchangeToQueueViaRoutingKey(exchangeConfig.Name, queueConfig.Name, "routing_key") 450 | 451 | require.NoError(t, err) 452 | }) 453 | 454 | t.Run("Binding non-existing exchange to existing queue via routing key", func(t *testing.T) { 455 | err = manager.BindExchangeToQueueViaRoutingKey("non_existing_exchange", queueConfig.Name, "routing_key") 456 | 457 | require.Error(t, err) 458 | assert.Equal(t, "Exception (404) Reason: \"NOT_FOUND - no exchange 'non_existing_exchange' in vhost '/'\"", err.Error()) 459 | }) 460 | 461 | t.Run("Binding existing exchange to non-existing queue via routing key", func(t *testing.T) { 462 | err = manager.BindExchangeToQueueViaRoutingKey(exchangeConfig.Name, "non_existing_queue", "routing_key") 463 | 464 | require.Error(t, err) 465 | assert.Equal(t, "Exception (404) Reason: \"NOT_FOUND - no queue 'non_existing_queue' in vhost '/'\"", err.Error()) 466 | }) 467 | 468 | require.NoError(t, manager.DeleteQueue(exchangeConfig.Name)) 469 | require.NoError(t, manager.DeleteQueue(queueConfig.Name)) 470 | 471 | require.NoError(t, manager.Disconnect()) 472 | } 473 | 474 | func (suite *ManagerTestSuite) TestGetNumberOfMessages() { 475 | t := suite.T() 476 | 477 | t.Setenv("RABBITMQ_HOST", suite.rabbitMQContainer.ContainerHost) 478 | t.Setenv("RABBITMQ_PORT", strconv.Itoa(int(suite.rabbitMQContainer.ContainerPort))) 479 | t.Setenv("RABBITMQ_USERNAME", suite.rabbitMQContainer.Username) 480 | t.Setenv("RABBITMQ_PASSWORD", suite.rabbitMQContainer.Password) 481 | 482 | manager, err := gorabbit.NewManagerFromEnv() 483 | 484 | require.NoError(t, err) 485 | assert.NotNil(t, manager) 486 | 487 | t.Run("Getting the number of messages from existing queue", func(t *testing.T) { 488 | queueConfig := gorabbit.QueueConfig{ 489 | Name: "test_queue", 490 | Durable: true, 491 | Exclusive: false, 492 | } 493 | 494 | err = manager.CreateQueue(queueConfig) 495 | 496 | require.NoError(t, err) 497 | 498 | exchangeConfig := gorabbit.ExchangeConfig{ 499 | Name: "test_exchange", 500 | Type: "topic", 501 | } 502 | 503 | err = manager.CreateExchange(exchangeConfig) 504 | 505 | require.NoError(t, err) 506 | 507 | err = manager.BindExchangeToQueueViaRoutingKey(exchangeConfig.Name, queueConfig.Name, "routing_key") 508 | 509 | require.NoError(t, err) 510 | 511 | count, countErr := manager.GetNumberOfMessages(queueConfig.Name) 512 | 513 | require.NoError(t, countErr) 514 | assert.Zero(t, count) 515 | 516 | require.NoError(t, manager.DeleteExchange(exchangeConfig.Name)) 517 | require.NoError(t, manager.DeleteQueue(queueConfig.Name)) 518 | }) 519 | 520 | t.Run("Getting the number of messages from non-existing queue", func(t *testing.T) { 521 | count, countErr := manager.GetNumberOfMessages("non_existing_queue") 522 | 523 | require.Error(t, countErr) 524 | assert.Equal(t, "Exception (404) Reason: \"NOT_FOUND - no queue 'non_existing_queue' in vhost '/'\"", countErr.Error()) 525 | assert.Equal(t, -1, count) 526 | }) 527 | 528 | require.NoError(t, manager.Disconnect()) 529 | } 530 | 531 | func (suite *ManagerTestSuite) TestPushMessageToExchange() { 532 | t := suite.T() 533 | 534 | t.Setenv("RABBITMQ_HOST", suite.rabbitMQContainer.ContainerHost) 535 | t.Setenv("RABBITMQ_PORT", strconv.Itoa(int(suite.rabbitMQContainer.ContainerPort))) 536 | t.Setenv("RABBITMQ_USERNAME", suite.rabbitMQContainer.Username) 537 | t.Setenv("RABBITMQ_PASSWORD", suite.rabbitMQContainer.Password) 538 | 539 | manager, err := gorabbit.NewManagerFromEnv() 540 | 541 | require.NoError(t, err) 542 | assert.NotNil(t, manager) 543 | 544 | t.Run("Push message to exchange", func(t *testing.T) { 545 | queueConfig := gorabbit.QueueConfig{ 546 | Name: "test_queue", 547 | Durable: true, 548 | Exclusive: false, 549 | } 550 | 551 | err = manager.CreateQueue(queueConfig) 552 | 553 | require.NoError(t, err) 554 | 555 | exchangeConfig := gorabbit.ExchangeConfig{ 556 | Name: "test_exchange", 557 | Type: "topic", 558 | } 559 | 560 | err = manager.CreateExchange(exchangeConfig) 561 | 562 | require.NoError(t, err) 563 | 564 | err = manager.BindExchangeToQueueViaRoutingKey(exchangeConfig.Name, queueConfig.Name, "routing_key") 565 | 566 | require.NoError(t, err) 567 | 568 | err = manager.PushMessageToExchange(exchangeConfig.Name, "routing_key", "Some message") 569 | 570 | // Small sleep for allowing message to be sent. 571 | time.Sleep(50 * time.Millisecond) 572 | 573 | require.NoError(t, err) 574 | 575 | count, countErr := manager.GetNumberOfMessages(queueConfig.Name) 576 | 577 | require.NoError(t, countErr) 578 | assert.Equal(t, 1, count) 579 | 580 | require.NoError(t, manager.PurgeQueue(queueConfig.Name)) 581 | 582 | count, countErr = manager.GetNumberOfMessages(queueConfig.Name) 583 | 584 | require.NoError(t, countErr) 585 | assert.Zero(t, count) 586 | 587 | require.NoError(t, manager.DeleteExchange(exchangeConfig.Name)) 588 | require.NoError(t, manager.DeleteQueue(queueConfig.Name)) 589 | }) 590 | 591 | t.Run("Pushing message to non-existing exchange should still work", func(t *testing.T) { 592 | err = manager.PushMessageToExchange("non_existing_exchange", "routing_key", "Some message") 593 | 594 | // Small sleep for allowing message to be sent. 595 | time.Sleep(50 * time.Millisecond) 596 | 597 | require.NoError(t, err) 598 | }) 599 | 600 | require.NoError(t, manager.Disconnect()) 601 | } 602 | 603 | func (suite *ManagerTestSuite) TestPopMessageFromQueue() { 604 | t := suite.T() 605 | 606 | t.Setenv("RABBITMQ_HOST", suite.rabbitMQContainer.ContainerHost) 607 | t.Setenv("RABBITMQ_PORT", strconv.Itoa(int(suite.rabbitMQContainer.ContainerPort))) 608 | t.Setenv("RABBITMQ_USERNAME", suite.rabbitMQContainer.Username) 609 | t.Setenv("RABBITMQ_PASSWORD", suite.rabbitMQContainer.Password) 610 | 611 | manager, err := gorabbit.NewManagerFromEnv() 612 | 613 | require.NoError(t, err) 614 | assert.NotNil(t, manager) 615 | 616 | t.Run("Push message to exchange and consume it", func(t *testing.T) { 617 | queueConfig := gorabbit.QueueConfig{ 618 | Name: "test_queue", 619 | Durable: true, 620 | Exclusive: false, 621 | } 622 | 623 | err = manager.CreateQueue(queueConfig) 624 | 625 | require.NoError(t, err) 626 | 627 | exchangeConfig := gorabbit.ExchangeConfig{ 628 | Name: "test_exchange", 629 | Type: "topic", 630 | } 631 | 632 | err = manager.CreateExchange(exchangeConfig) 633 | 634 | require.NoError(t, err) 635 | 636 | err = manager.BindExchangeToQueueViaRoutingKey(exchangeConfig.Name, queueConfig.Name, "routing_key") 637 | 638 | require.NoError(t, err) 639 | 640 | err = manager.PushMessageToExchange(exchangeConfig.Name, "routing_key", "Some message") 641 | 642 | // Small sleep for allowing message to be sent. 643 | time.Sleep(50 * time.Millisecond) 644 | 645 | require.NoError(t, err) 646 | 647 | count, countErr := manager.GetNumberOfMessages(queueConfig.Name) 648 | 649 | require.NoError(t, countErr) 650 | assert.Equal(t, 1, count) 651 | 652 | delivery, popErr := manager.PopMessageFromQueue(queueConfig.Name, true) 653 | 654 | require.NoError(t, popErr) 655 | assert.Equal(t, "\"Some message\"", string(delivery.Body)) 656 | 657 | require.NoError(t, manager.PurgeQueue(queueConfig.Name)) 658 | 659 | count, countErr = manager.GetNumberOfMessages(queueConfig.Name) 660 | 661 | require.NoError(t, countErr) 662 | assert.Zero(t, count) 663 | 664 | require.NoError(t, manager.DeleteExchange(exchangeConfig.Name)) 665 | require.NoError(t, manager.DeleteQueue(queueConfig.Name)) 666 | }) 667 | 668 | t.Run("Popping message from non-existing queue", func(t *testing.T) { 669 | delivery, popErr := manager.PopMessageFromQueue("non_existing_queue", true) 670 | 671 | require.Error(t, popErr) 672 | assert.Nil(t, delivery) 673 | assert.Equal(t, "Exception (404) Reason: \"NOT_FOUND - no queue 'non_existing_queue' in vhost '/'\"", popErr.Error()) 674 | }) 675 | 676 | t.Run("Popping message from existent empty queue", func(t *testing.T) { 677 | queueConfig := gorabbit.QueueConfig{ 678 | Name: "test_queue", 679 | Durable: true, 680 | Exclusive: false, 681 | } 682 | 683 | err = manager.CreateQueue(queueConfig) 684 | 685 | require.NoError(t, err) 686 | 687 | delivery, popErr := manager.PopMessageFromQueue(queueConfig.Name, true) 688 | 689 | require.Error(t, popErr) 690 | require.Equal(t, "queue is empty", popErr.Error()) 691 | assert.Nil(t, delivery) 692 | 693 | require.NoError(t, manager.DeleteQueue(queueConfig.Name)) 694 | }) 695 | 696 | require.NoError(t, manager.Disconnect()) 697 | } 698 | 699 | func (suite *ManagerTestSuite) TestSetupFromDefinitions() { 700 | t := suite.T() 701 | 702 | t.Setenv("RABBITMQ_HOST", suite.rabbitMQContainer.ContainerHost) 703 | t.Setenv("RABBITMQ_PORT", strconv.Itoa(int(suite.rabbitMQContainer.ContainerPort))) 704 | t.Setenv("RABBITMQ_USERNAME", suite.rabbitMQContainer.Username) 705 | t.Setenv("RABBITMQ_PASSWORD", suite.rabbitMQContainer.Password) 706 | 707 | manager, err := gorabbit.NewManagerFromEnv() 708 | 709 | require.NoError(t, err) 710 | assert.NotNil(t, manager) 711 | 712 | t.Run("Setting up from definitions with wrong path", func(t *testing.T) { 713 | err = manager.SetupFromDefinitions("wrong-path.json") 714 | 715 | require.Error(t, err) 716 | assert.Equal(t, "open wrong-path.json: no such file or directory", err.Error()) 717 | }) 718 | 719 | t.Run("Setting up from definitions with right path", func(t *testing.T) { 720 | err = manager.SetupFromDefinitions("assets/definitions.example.json") 721 | 722 | require.NoError(t, err) 723 | }) 724 | 725 | require.NoError(t, manager.Disconnect()) 726 | } 727 | 728 | func TestManagerTestSuite(t *testing.T) { 729 | suite.Run(t, new(ManagerTestSuite)) 730 | } 731 | -------------------------------------------------------------------------------- /marshalling.go: -------------------------------------------------------------------------------- 1 | package gorabbit 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | type Marshaller interface { 9 | ContentType() string 10 | Marshal(data any) ([]byte, error) 11 | } 12 | 13 | type marshaller struct { 14 | contentType string 15 | marshal func(data any) ([]byte, error) 16 | } 17 | 18 | func (m *marshaller) ContentType() string { 19 | return m.contentType 20 | } 21 | 22 | func (m *marshaller) Marshal(data any) ([]byte, error) { 23 | return m.marshal(data) 24 | } 25 | 26 | func NewJSONMarshaller() Marshaller { 27 | return &marshaller{ 28 | contentType: "application/json", 29 | marshal: json.Marshal, 30 | } 31 | } 32 | 33 | func NewTextMarshaller() Marshaller { 34 | return &marshaller{ 35 | contentType: "text/plain", 36 | marshal: func(data any) ([]byte, error) { 37 | switch s := data.(type) { 38 | case string: 39 | return []byte(s), nil 40 | case []byte: 41 | return s, nil 42 | default: 43 | return nil, fmt.Errorf("cannot marshal %T as text", data) 44 | } 45 | }, 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /marshalling_test.go: -------------------------------------------------------------------------------- 1 | package gorabbit_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/KardinalAI/gorabbit" 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestJSONMarshaller(t *testing.T) { 12 | m := gorabbit.NewJSONMarshaller() 13 | assert.NotNil(t, m) 14 | 15 | assert.Equal(t, "application/json", m.ContentType()) 16 | 17 | data, err := m.Marshal("test") 18 | require.NoError(t, err) 19 | assert.Equal(t, []byte(`"test"`), data) 20 | } 21 | 22 | func TestTextMarshaller(t *testing.T) { 23 | m := gorabbit.NewTextMarshaller() 24 | assert.NotNil(t, m) 25 | 26 | assert.Equal(t, "text/plain", m.ContentType()) 27 | 28 | data, err := m.Marshal("test") 29 | require.NoError(t, err) 30 | assert.Equal(t, []byte(`test`), data) 31 | } 32 | -------------------------------------------------------------------------------- /model.go: -------------------------------------------------------------------------------- 1 | package gorabbit 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | 7 | amqp "github.com/rabbitmq/amqp091-go" 8 | ) 9 | 10 | type SchemaDefinitions struct { 11 | Exchanges []struct { 12 | Name string `json:"name"` 13 | Vhost string `json:"vhost"` 14 | Type string `json:"type"` 15 | Durable bool `json:"durable"` 16 | AutoDelete bool `json:"auto_delete"` 17 | Internal bool `json:"internal"` 18 | Arguments struct { 19 | } `json:"arguments"` 20 | } `json:"exchanges"` 21 | Queues []struct { 22 | Name string `json:"name"` 23 | Vhost string `json:"vhost"` 24 | Durable bool `json:"durable"` 25 | AutoDelete bool `json:"auto_delete"` 26 | Arguments struct { 27 | } `json:"arguments"` 28 | } `json:"queues"` 29 | Bindings []struct { 30 | Source string `json:"source"` 31 | Vhost string `json:"vhost"` 32 | Destination string `json:"destination"` 33 | DestinationType string `json:"destination_type"` 34 | RoutingKey string `json:"routing_key"` 35 | Arguments struct { 36 | } `json:"arguments"` 37 | } `json:"bindings"` 38 | } 39 | 40 | type ExchangeConfig struct { 41 | Name string `yaml:"name"` 42 | Type ExchangeType `yaml:"type"` 43 | Persisted bool `yaml:"persisted"` 44 | Args map[string]interface{} `yaml:"args"` 45 | } 46 | 47 | type QueueConfig struct { 48 | Name string `yaml:"name"` 49 | Durable bool `yaml:"durable"` 50 | Exclusive bool `yaml:"exclusive"` 51 | AutoDelete bool `yaml:"autoDelete"` 52 | Args map[string]interface{} `yaml:"args"` 53 | Bindings []BindingConfig `yaml:"bindings"` 54 | } 55 | 56 | type BindingConfig struct { 57 | RoutingKey string `yaml:"routing_key"` 58 | Exchange string `yaml:"exchange"` 59 | } 60 | 61 | type PublishingOptions struct { 62 | MessagePriority *MessagePriority 63 | DeliveryMode *DeliveryMode 64 | TTL *time.Duration 65 | } 66 | 67 | func SendOptions() *PublishingOptions { 68 | return &PublishingOptions{} 69 | } 70 | 71 | func (m *PublishingOptions) priority() uint8 { 72 | if m.MessagePriority == nil { 73 | return PriorityMedium.Uint8() 74 | } 75 | 76 | return m.MessagePriority.Uint8() 77 | } 78 | 79 | func (m *PublishingOptions) mode() uint8 { 80 | if m.DeliveryMode == nil { 81 | return Persistent.Uint8() 82 | } 83 | 84 | return m.DeliveryMode.Uint8() 85 | } 86 | 87 | func (m *PublishingOptions) ttl() string { 88 | if m.TTL == nil { 89 | return "" 90 | } 91 | 92 | return strconv.FormatInt(m.TTL.Milliseconds(), 10) 93 | } 94 | 95 | func (m *PublishingOptions) SetPriority(priority MessagePriority) *PublishingOptions { 96 | m.MessagePriority = &priority 97 | 98 | return m 99 | } 100 | 101 | func (m *PublishingOptions) SetMode(mode DeliveryMode) *PublishingOptions { 102 | m.DeliveryMode = &mode 103 | 104 | return m 105 | } 106 | 107 | func (m *PublishingOptions) SetTTL(ttl time.Duration) *PublishingOptions { 108 | m.TTL = &ttl 109 | 110 | return m 111 | } 112 | 113 | type consumptionHealth map[string]bool 114 | 115 | func (s consumptionHealth) IsHealthy() bool { 116 | for _, v := range s { 117 | if !v { 118 | return false 119 | } 120 | } 121 | 122 | return true 123 | } 124 | 125 | func (s consumptionHealth) AddSubscription(queue string, err error) { 126 | s[queue] = err == nil 127 | } 128 | 129 | type mqttPublishing struct { 130 | Exchange string 131 | RoutingKey string 132 | Mandatory bool 133 | Immediate bool 134 | Msg amqp.Publishing 135 | } 136 | 137 | func (m mqttPublishing) HashCode() string { 138 | return m.Msg.MessageId 139 | } 140 | 141 | type RabbitMQEnvs struct { 142 | Host string `env:"RABBITMQ_HOST"` 143 | Port uint `env:"RABBITMQ_PORT"` 144 | Username string `env:"RABBITMQ_USERNAME"` 145 | Password string `env:"RABBITMQ_PASSWORD"` 146 | Vhost string `env:"RABBITMQ_VHOST"` 147 | UseTLS bool `env:"RABBITMQ_USE_TLS"` 148 | ConnectionName string `env:"RABBITMQ_CONNECTION_NAME"` 149 | } 150 | -------------------------------------------------------------------------------- /ttl_map.go: -------------------------------------------------------------------------------- 1 | package gorabbit 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | type ttlMapValue[V any] struct { 9 | value V 10 | createdAt time.Time 11 | } 12 | 13 | type ttlMap[K comparable, V any] struct { 14 | m map[K]ttlMapValue[V] 15 | l sync.Mutex 16 | } 17 | 18 | func newTTLMap[K comparable, V any](ln uint64, maxTTL time.Duration) *ttlMap[K, V] { 19 | m := &ttlMap[K, V]{m: make(map[K]ttlMapValue[V], ln)} 20 | 21 | go func() { 22 | const tickFraction = 3 23 | 24 | for now := range time.Tick(maxTTL / tickFraction) { 25 | m.l.Lock() 26 | for k := range m.m { 27 | issueDate := m.m[k].createdAt 28 | if now.Sub(issueDate) >= maxTTL { 29 | delete(m.m, k) 30 | } 31 | } 32 | m.l.Unlock() 33 | } 34 | }() 35 | 36 | return m 37 | } 38 | 39 | func (m *ttlMap[K, V]) Len() int { 40 | return len(m.m) 41 | } 42 | 43 | func (m *ttlMap[K, V]) Put(k K, v V) { 44 | m.l.Lock() 45 | 46 | defer m.l.Unlock() 47 | 48 | if _, ok := m.m[k]; !ok { 49 | m.m[k] = ttlMapValue[V]{value: v, createdAt: time.Now()} 50 | } 51 | } 52 | 53 | func (m *ttlMap[K, V]) Get(k K) (V, bool) { 54 | m.l.Lock() 55 | 56 | defer m.l.Unlock() 57 | 58 | v, found := m.m[k] 59 | 60 | innerVal := v.value 61 | 62 | return innerVal, found 63 | } 64 | 65 | func (m *ttlMap[K, V]) ForEach(process func(k K, v V)) { 66 | for key, value := range m.m { 67 | innerVal := value.value 68 | 69 | process(key, innerVal) 70 | } 71 | } 72 | 73 | func (m *ttlMap[K, V]) Delete(k K) { 74 | m.l.Lock() 75 | 76 | defer m.l.Unlock() 77 | 78 | delete(m.m, k) 79 | } 80 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package gorabbit 2 | 3 | import ( 4 | "errors" 5 | 6 | amqp "github.com/rabbitmq/amqp091-go" 7 | ) 8 | 9 | // Error Utils. 10 | const ( 11 | codeNotFound = 404 12 | ) 13 | 14 | // isErrorNotFound checks if the error returned by a connection or channel has the 404 code. 15 | func isErrorNotFound(err error) bool { 16 | var amqpError *amqp.Error 17 | 18 | errors.As(err, &amqpError) 19 | 20 | if amqpError == nil { 21 | return false 22 | } 23 | 24 | return amqpError.Code == codeNotFound 25 | } 26 | --------------------------------------------------------------------------------