From 90e7b8dd57c49d769f5cd9b66162fd5b6ecf51c2 Mon Sep 17 00:00:00 2001 From: comicrime Date: Sat, 29 Aug 2026 15:38:38 +0300 Subject: [PATCH 1/2] added logging middleware --- caller/middleware/logger.go | 157 +++++++++++++++++++++++++++++++ caller/middleware/logger_test.go | 18 ++++ 2 files changed, 175 insertions(+) create mode 100644 caller/middleware/logger_test.go diff --git a/caller/middleware/logger.go b/caller/middleware/logger.go index c870d7c..889e7b3 100644 --- a/caller/middleware/logger.go +++ b/caller/middleware/logger.go @@ -1 +1,158 @@ package middleware + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "time" +) + +type LogFunc func(context.Context, string, ...any) + +type loggerConfig struct { + logFunc LogFunc + errLogFunc LogFunc + respLogFunc LogFunc + + logHeaders, logReq, logResp, logErr bool + + prefix string + args []any +} + +func newDefaulLoggerConfig() *loggerConfig { + return &loggerConfig{ + logFunc: DiscardLoggerFunc, + } +} + +type LoggerOption func(d *loggerConfig) + +func WithRequest() LoggerOption { + return func(d *loggerConfig) { + d.logReq = true + } +} + +func WithHeader() LoggerOption { + return func(d *loggerConfig) { + d.logHeaders = true + } +} + +func WithResponse(f LogFunc) LoggerOption { + return func(d *loggerConfig) { + d.logResp = true + d.respLogFunc = f + } +} + +func WithError(f LogFunc) LoggerOption { + return func(d *loggerConfig) { + d.logErr = true + d.errLogFunc = f + } +} + +func WithPrefix(prefix string) LoggerOption { + return func(d *loggerConfig) { + d.prefix = prefix + } +} + +func WithLogArgs(args ...any) LoggerOption { + return func(d *loggerConfig) { + d.args = args + } +} + +func DiscardLoggerFunc(_ context.Context, _ string, _ ...any) { return } + +func Logger(opts ...LoggerOption) func(http.RoundTripper) http.RoundTripper { + cfg := newDefaulLoggerConfig() + for _, o := range opts { + o(cfg) + } + + return func(next http.RoundTripper) http.RoundTripper { + return RoundTripperFunc(func(r *http.Request) (*http.Response, error) { + st := time.Now() + + logMessage := fmt.Sprintf("%sclient", wrapPrefix(cfg.prefix)) + logParts := []any{"method", r.Method, "target", r.URL} + + { + reqLogParts := []any{logParts} + if cfg.logHeaders && len(r.Header) > 0 { + headerLog, err := json.Marshal(r.Header) + if err != nil { + headerLog = fmt.Appendf(nil, "%v", r.Header) + } + reqLogParts = append(reqLogParts, "headers", headerLog) + } + + if cfg.logReq && r.Body != nil { + var bodyLog string + body, e := io.ReadAll(r.Body) + if e == nil { + _ = r.Body.Close() + r.Body = io.NopCloser(bytes.NewReader(body)) + bodyLog = string(body) + if len(bodyLog) > 1024 { + bodyLog = bodyLog[:1024] + "..." + } + bodyLog = strings.ReplaceAll(bodyLog, "\n", " ") + } + if bodyLog != "" { + reqLogParts = append(reqLogParts, "request body", bodyLog) + } + } + cfg.logFunc(r.Context(), logMessage, reqLogParts...) + } + + if !cfg.logResp && !cfg.logErr { + return next.RoundTrip(r) + } + + respLogParts := []any{logParts} + resp, err := next.RoundTrip(r) + respLogParts = append(respLogParts, "duration", time.Since(st).String()) + if err != nil && cfg.logErr { + cfg.errLogFunc(r.Context(), logMessage, "error", err.Error()) + } + respLogParts = append(respLogParts, "code", resp.StatusCode) + + if err != nil && resp.Body != nil { + var bodyLog string + body, e := io.ReadAll(resp.Body) + if e == nil { + _ = resp.Body.Close() + resp.Body = io.NopCloser(bytes.NewReader(body)) + bodyLog = string(body) + if len(bodyLog) > 1024 { + bodyLog = bodyLog[:1024] + "..." + } + bodyLog = strings.ReplaceAll(bodyLog, "\n", " ") + } + if bodyLog != "" { + respLogParts = append(respLogParts, "response body", bodyLog) + } + } + + cfg.respLogFunc(r.Context(), logMessage, respLogParts...) + return resp, err + }) + } +} + +func wrapPrefix(prefix string) string { + if prefix == "" { + return "" + } + + return prefix + " " +} diff --git a/caller/middleware/logger_test.go b/caller/middleware/logger_test.go new file mode 100644 index 0000000..37961f0 --- /dev/null +++ b/caller/middleware/logger_test.go @@ -0,0 +1,18 @@ +package middleware + +import ( + "log/slog" + "net/http" + "testing" +) + +func TestLogger(t *testing.T) { + r := &http.Request{ + Header: http.Header{ + "h1": []string{"1", "2", "3"}, + "h2": []string{"qwe"}, + }, + } + + slog.Info("123", "headers", r.Header) +} From def9d146d3059e8a7fd56ec804a60293e2beca3a Mon Sep 17 00:00:00 2001 From: comicrime Date: Sat, 29 Aug 2026 16:09:49 +0300 Subject: [PATCH 2/2] DRC-57: updated project structure. implemented logger and test for it. updated CI/CD templates --- .github/workflows/ci.yml | 80 +++++- .github/workflows/release.yml | 85 ++++-- .gitignore | 1 + Makefile | 17 +- README.md | 18 +- caller/go.mod | 29 ++ caller/go.sum | 224 +++++++++++++++ caller/middleware/logger.go | 147 +++++++--- caller/middleware/logger_compat_test.go | 118 ++++++++ caller/middleware/logger_test.go | 362 +++++++++++++++++++++++- go.work | 7 + integration/discovery_test.go | 183 ++++++++++++ integration/go.mod | 36 +++ integration/go.sum | 224 +++++++++++++++ go.mod => registry/go.mod | 2 +- go.sum => registry/go.sum | 0 registry/registry_test.go | 73 ----- 17 files changed, 1428 insertions(+), 178 deletions(-) create mode 100644 .gitignore create mode 100644 caller/go.mod create mode 100644 caller/go.sum create mode 100644 caller/middleware/logger_compat_test.go create mode 100644 go.work create mode 100644 integration/discovery_test.go create mode 100644 integration/go.mod create mode 100644 integration/go.sum rename go.mod => registry/go.mod (95%) rename go.sum => registry/go.sum (100%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9bdcacd..ca86728 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,10 @@ on: # there does not need a second one branches-ignore: [master] workflow_call: + outputs: + modules: + description: Every module directory in the repo + value: ${{ jobs.modules.outputs.all }} # A branch that is pushed again supersedes its own in-flight run, but master is left # alone: its run gates the release. @@ -16,49 +20,97 @@ concurrency: permissions: contents: read +# Each module is built and tested the way a consumer gets it - resolved from its own +# go.mod - rather than through the workspace, which would paper over a missing require. +env: + GOWORK: "off" + jobs: + # Modules are discovered rather than listed, so adding one to the repo puts it under + # CI without touching this file. + modules: + runs-on: ubuntu-latest + outputs: + all: ${{ steps.find.outputs.all }} + steps: + - uses: actions/checkout@v7 + + - name: Find the modules + id: find + run: | + set -euo pipefail + + all=$(find . -name go.mod -not -path './.git/*' -printf '%h\n' \ + | sed 's|^\./||' \ + | sort \ + | jq -R -s -c 'split("\n") | map(select(length > 0))') + + echo "all=$all" >> "$GITHUB_OUTPUT" + echo "Modules: $all" + lint: + needs: modules runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + module: ${{ fromJSON(needs.modules.outputs.all) }} steps: - uses: actions/checkout@v7 - uses: actions/setup-go@v7 with: - go-version-file: go.mod + go-version-file: ${{ matrix.module }}/go.mod - - name: Tool versions + - name: Tool version id: tools - run: | - echo "golangci=$(make -s print-GOLANGCI_VERSION)" >> "$GITHUB_OUTPUT" - echo "actionlint=$(make -s print-ACTIONLINT_VERSION)" >> "$GITHUB_OUTPUT" + run: echo "golangci=$(make -s print-GOLANGCI_VERSION)" >> "$GITHUB_OUTPUT" + # the root .golangci.yml applies to every module: golangci-lint walks up for it - uses: golangci/golangci-lint-action@v9 with: version: ${{ steps.tools.outputs.golangci }} - - # the other half of `make lint`, so CI is never weaker than the local target - - name: Lint the workflows - env: - ACTIONLINT_VERSION: ${{ steps.tools.outputs.actionlint }} - run: | - go install "github.com/rhysd/actionlint/cmd/actionlint@${ACTIONLINT_VERSION}" - "$(go env GOPATH)/bin/actionlint" + working-directory: ${{ matrix.module }} - name: go.mod and go.sum are tidy + working-directory: ${{ matrix.module }} run: | go mod tidy git diff --exit-code go.mod go.sum + # One repo, one set of workflows - not per module. + workflows: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - uses: actions/setup-go@v7 + with: + go-version-file: caller/go.mod + + - name: Lint the workflows + run: | + set -euo pipefail + go install "github.com/rhysd/actionlint/cmd/actionlint@$(make -s print-ACTIONLINT_VERSION)" + "$(go env GOPATH)/bin/actionlint" + test: + needs: modules runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + module: ${{ fromJSON(needs.modules.outputs.all) }} steps: - uses: actions/checkout@v7 - uses: actions/setup-go@v7 with: - go-version-file: go.mod + go-version-file: ${{ matrix.module }}/go.mod - run: go build ./... + working-directory: ${{ matrix.module }} # -race because the resolver cache and the registry heartbeat are concurrent - run: go test -race -count=1 ./... + working-directory: ${{ matrix.module }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e91f7d8..885a264 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,7 +5,7 @@ on: branches: [master] # Releases are serialised, never cancelled: two merges landing together must each get -# their turn at the tag rather than one clobbering the other's run. +# their turn at the tags rather than one clobbering the other's run. concurrency: group: release cancel-in-progress: false @@ -26,43 +26,68 @@ jobs: steps: - uses: actions/checkout@v7 with: - fetch-depth: 0 # the whole tag history, to find the version to bump + fetch-depth: 0 # the whole tag history, to find each module's version to bump - - name: Work out the next version - id: version - run: | - set -euo pipefail - - # A re-run of this workflow, or a commit tagged by hand, must not publish a - # second version of the same code. - if tag=$(git describe --exact-match --tags HEAD 2>/dev/null); then - echo "HEAD is already released as $tag, nothing to do" - exit 0 - fi - - latest=$(git tag -l 'v*' --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -1 || true) - if [ -z "$latest" ]; then - next=v0.1.0 - else - IFS=. read -r major minor patch <<<"${latest#v}" - next="v${major}.${minor}.$((patch + 1))" - fi - - echo "next=$next" >> "$GITHUB_OUTPUT" - echo "Releasing $next (previous: ${latest:-none})" + - uses: actions/setup-go@v7 + with: + go-version-file: caller/go.mod - - name: Tag and release - if: steps.version.outputs.next != '' + # Every module is versioned on its own, so a change to one does not drag the + # others through a release they have no new code for. Tags carry the module's + # directory as a prefix, which is how the go command resolves a nested module. + - name: Tag the modules whose files changed env: GH_TOKEN: ${{ github.token }} - NEXT: ${{ steps.version.outputs.next }} + MODULES: ${{ needs.ci.outputs.modules }} run: | set -euo pipefail git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git tag -a "$NEXT" -m "$NEXT" - git push origin "$NEXT" + tagged=() + while read -r dir; do + # a module that replaces another by local path cannot be consumed from + # outside the repo, so it is built and tested but never published + replaces=$(go mod edit -json "$dir/go.mod" \ + | jq '[.Replace // [] | .[] | select(.New.Path | test("^\\.{1,2}/"))] | length') + if [ "$replaces" -ne 0 ]; then + echo "$dir: not publishable (replaces a module by local path), skipping" + continue + fi + + # A re-run of this workflow, or a tag pushed by hand, must not publish a + # second version of the same code. + existing=$(git tag --points-at HEAD --list "$dir/v*" | head -1) + if [ -n "$existing" ]; then + echo "$dir: HEAD is already released as $existing, skipping" + continue + fi + + latest=$(git tag --list "$dir/v*" --sort=-v:refname \ + | grep -E "^$dir/v[0-9]+\.[0-9]+\.[0-9]+$" | head -1 || true) + + if [ -z "$latest" ]; then + next="$dir/v0.1.0" + elif git diff --quiet "$latest" HEAD -- "$dir"; then + echo "$dir: unchanged since $latest, skipping" + continue + else + IFS=. read -r major minor patch <<<"${latest#"$dir/v"}" + next="$dir/v${major}.${minor}.$((patch + 1))" + fi + + echo "$dir: releasing $next (previous: ${latest:-none})" + git tag -a "$next" -m "$next" + tagged+=("$next") + done < <(jq -r '.[]' <<<"$MODULES") + + if [ ${#tagged[@]} -eq 0 ]; then + echo "No module changed, nothing to release" + exit 0 + fi - gh release create "$NEXT" --title "$NEXT" --generate-notes + git push origin "${tagged[@]}" + for tag in "${tagged[@]}"; do + gh release create "$tag" --title "$tag" --generate-notes + done diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eb97422 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +go.work.sum diff --git a/Makefile b/Makefile index 7a9df1b..7d0781b 100644 --- a/Makefile +++ b/Makefile @@ -3,13 +3,24 @@ ACTIONLINT_VERSION ?= v1.7.12 GOBIN ?= $(or $(shell go env GOBIN),$(shell go env GOPATH)/bin) +# Discovered rather than listed, so a new module is covered without editing this file. +MODULES := $(patsubst ./%/go.mod,%,$(shell find . -name go.mod -not -path './.git/*' | sort)) + +# GOWORK=off so each module is exercised the way a consumer gets it, resolved from its +# own go.mod rather than through the workspace. .PHONY: test lint install-tools -test: - go test -v ./... +test: + @for m in $(MODULES); do \ + echo "== $$m"; \ + (cd $$m && GOWORK=off go test -v ./...) || exit 1; \ + done lint: - $(GOBIN)/golangci-lint run ./... + @for m in $(MODULES); do \ + echo "== $$m"; \ + (cd $$m && GOWORK=off $(GOBIN)/golangci-lint run ./...) || exit 1; \ + done $(GOBIN)/actionlint install-tools: diff --git a/README.md b/README.md index ed2ed01..75d2da5 100644 --- a/README.md +++ b/README.md @@ -1 +1,17 @@ -# Callpack \ No newline at end of file +# Callpack + +Modules that I constantly implement in my projects: and which I decided to collect in one place for easy reuse! + +| Module | Import path | What it does | +| --- | --- | --- | +| `caller` | `github.com/draincloud/callpack/caller` | `http.Client` fancy wrapper with a round-tripper middleware chain: headers, logging, etc | +| `registry` | `github.com/draincloud/callpack/registry` | Registers a service instance in consul and heartbeats its TTL check. | + +```sh +go get github.com/draincloud/callpack/caller +go get github.com/draincloud/callpack/registry +``` + +Each is tagged with its own prefix — `caller/v0.1.0`, `registry/v0.1.0` + +`integration/` holds the tests that exercise the two against each other. \ No newline at end of file diff --git a/caller/go.mod b/caller/go.mod new file mode 100644 index 0000000..edcf388 --- /dev/null +++ b/caller/go.mod @@ -0,0 +1,29 @@ +module github.com/draincloud/callpack/caller + +go 1.25.0 + +require ( + github.com/draincloud/logger v0.0.4 + github.com/hashicorp/consul/api v1.32.1 + github.com/stretchr/testify v1.12.1 +) + +require ( + github.com/armon/go-metrics v0.4.1 // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-hclog v1.5.0 // indirect + github.com/hashicorp/go-immutable-radix v1.3.1 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-rootcerts v1.0.2 // indirect + github.com/hashicorp/golang-lru v0.5.4 // indirect + github.com/hashicorp/serf v0.10.1 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect + golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect + golang.org/x/sys v0.31.0 // indirect +) diff --git a/caller/go.sum b/caller/go.sum new file mode 100644 index 0000000..4bcf8c3 --- /dev/null +++ b/caller/go.sum @@ -0,0 +1,224 @@ +github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA= +github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/draincloud/logger v0.0.4 h1:BEbNyy8AkuqQy60uST7O9IgW8ofl1zI6iN4tygFTwAg= +github.com/draincloud/logger v0.0.4/go.mod h1:t/vRd80p4+3cQg7HlmRCm5/GSTHfTWDC0WWANdtKYMw= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= +github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/hashicorp/consul/api v1.32.1 h1:0+osr/3t/aZNAdJX558crU3PEjVrG4x6715aZHRgceE= +github.com/hashicorp/consul/api v1.32.1/go.mod h1:mXUWLnxftwTmDv4W3lzxYCPD199iNLLUyLfLGFJbtl4= +github.com/hashicorp/consul/sdk v0.16.1 h1:V8TxTnImoPD5cj0U9Spl0TUxcytjcbbJeADFF07KdHg= +github.com/hashicorp/consul/sdk v0.16.1/go.mod h1:fSXvwxB2hmh1FMZCNl6PwX0Q/1wdWtHJcZ7Ea5tns0s= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= +github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-msgpack v0.5.5 h1:i9R9JSrqIz0QVLz3sz+i3YJdT7TTSLcfLLzJi9aZTuI= +github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= +github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI= +github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= +github.com/hashicorp/memberlist v0.5.0 h1:EtYPN8DpAURiapus508I4n9CzHs2W+8NZGbmmR/prTM= +github.com/hashicorp/memberlist v0.5.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0= +github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY= +github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= +github.com/miekg/dns v1.1.41 h1:WMszZWJG0XmzbK9FEmzH2TVcqYzFesusSIB41b8KHxY= +github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= +github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= +github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/caller/middleware/logger.go b/caller/middleware/logger.go index 889e7b3..02ce794 100644 --- a/caller/middleware/logger.go +++ b/caller/middleware/logger.go @@ -7,10 +7,16 @@ import ( "fmt" "io" "net/http" + "slices" "strings" "time" ) +// maxBodyLog is how much of a body a log line keeps; anything longer is truncated. +const maxBodyLog = 1024 + +// LogFunc is the shape of a context-aware key-value logging call, matching +// slog.Logger.InfoContext and friends so a logger can be handed over directly. type LogFunc func(context.Context, string, ...any) type loggerConfig struct { @@ -32,18 +38,31 @@ func newDefaulLoggerConfig() *loggerConfig { type LoggerOption func(d *loggerConfig) +// WithLogger sets the function the request line is logged with. Without it requests are +// discarded, leaving WithRequest and WithHeader nothing to log to. +func WithLogger(f LogFunc) LoggerOption { + return func(d *loggerConfig) { + d.logFunc = f + } +} + +// WithRequest adds the request body to the request line. func WithRequest() LoggerOption { return func(d *loggerConfig) { d.logReq = true } } +// WithHeader adds the request headers to the request line, with the values of headers +// that carry credentials redacted. func WithHeader() LoggerOption { return func(d *loggerConfig) { d.logHeaders = true } } +// WithResponse logs the outcome of the round trip - status code, duration and response +// body - with f. func WithResponse(f LogFunc) LoggerOption { return func(d *loggerConfig) { d.logResp = true @@ -51,6 +70,8 @@ func WithResponse(f LogFunc) LoggerOption { } } +// WithError logs a failed round trip with f instead of the response function, so +// failures can go to a louder level than successes. func WithError(f LogFunc) LoggerOption { return func(d *loggerConfig) { d.logErr = true @@ -58,20 +79,25 @@ func WithError(f LogFunc) LoggerOption { } } +// WithPrefix puts prefix in front of every message this middleware logs. func WithPrefix(prefix string) LoggerOption { return func(d *loggerConfig) { d.prefix = prefix } } +// WithLogArgs adds key-value pairs to every line this middleware logs. func WithLogArgs(args ...any) LoggerOption { return func(d *loggerConfig) { d.args = args } } -func DiscardLoggerFunc(_ context.Context, _ string, _ ...any) { return } +func DiscardLoggerFunc(_ context.Context, _ string, _ ...any) {} +// Logger logs outgoing requests and their outcome. A round trip logs one request line +// and, once WithResponse or WithError is set, exactly one outcome line: the error +// function when the trip failed and WithError is set, the response function otherwise. func Logger(opts ...LoggerOption) func(http.RoundTripper) http.RoundTripper { cfg := newDefaulLoggerConfig() for _, o := range opts { @@ -83,72 +109,99 @@ func Logger(opts ...LoggerOption) func(http.RoundTripper) http.RoundTripper { st := time.Now() logMessage := fmt.Sprintf("%sclient", wrapPrefix(cfg.prefix)) - logParts := []any{"method", r.Method, "target", r.URL} - - { - reqLogParts := []any{logParts} - if cfg.logHeaders && len(r.Header) > 0 { - headerLog, err := json.Marshal(r.Header) - if err != nil { - headerLog = fmt.Appendf(nil, "%v", r.Header) - } - reqLogParts = append(reqLogParts, "headers", headerLog) - } + // Redacted keeps a password embedded in the URL out of the log, and renders as a + // string under a JSON handler, where a *url.URL would land as a struct + logParts := append([]any{"method", r.Method, "target", r.URL.Redacted()}, cfg.args...) - if cfg.logReq && r.Body != nil { - var bodyLog string - body, e := io.ReadAll(r.Body) - if e == nil { - _ = r.Body.Close() - r.Body = io.NopCloser(bytes.NewReader(body)) - bodyLog = string(body) - if len(bodyLog) > 1024 { - bodyLog = bodyLog[:1024] + "..." - } - bodyLog = strings.ReplaceAll(bodyLog, "\n", " ") - } - if bodyLog != "" { - reqLogParts = append(reqLogParts, "request body", bodyLog) - } + reqLogParts := slices.Clone(logParts) + if cfg.logHeaders && len(r.Header) > 0 { + reqLogParts = append(reqLogParts, "headers", headerLog(r.Header)) + } + if cfg.logReq && r.Body != nil { + var bodyLog string + bodyLog, r.Body = drainForLog(r.Body) + if bodyLog != "" { + reqLogParts = append(reqLogParts, "request body", bodyLog) } - cfg.logFunc(r.Context(), logMessage, reqLogParts...) } + cfg.logFunc(r.Context(), logMessage, reqLogParts...) if !cfg.logResp && !cfg.logErr { return next.RoundTrip(r) } - respLogParts := []any{logParts} resp, err := next.RoundTrip(r) - respLogParts = append(respLogParts, "duration", time.Since(st).String()) - if err != nil && cfg.logErr { - cfg.errLogFunc(r.Context(), logMessage, "error", err.Error()) + outLogParts := append(slices.Clone(logParts), "duration", time.Since(st).String()) + + // a failed round trip has no response to report on, so the outcome line is + // the error - logged by the error function when there is one + if err != nil { + outLogParts = append(outLogParts, "error", err.Error()) + outLogFunc := cfg.respLogFunc + if cfg.logErr { + outLogFunc = cfg.errLogFunc + } + outLogFunc(r.Context(), logMessage, outLogParts...) + return resp, err } - respLogParts = append(respLogParts, "code", resp.StatusCode) - if err != nil && resp.Body != nil { + if !cfg.logResp { + return resp, nil + } + + outLogParts = append(outLogParts, "code", resp.StatusCode) + if resp.Body != nil { var bodyLog string - body, e := io.ReadAll(resp.Body) - if e == nil { - _ = resp.Body.Close() - resp.Body = io.NopCloser(bytes.NewReader(body)) - bodyLog = string(body) - if len(bodyLog) > 1024 { - bodyLog = bodyLog[:1024] + "..." - } - bodyLog = strings.ReplaceAll(bodyLog, "\n", " ") - } + bodyLog, resp.Body = drainForLog(resp.Body) if bodyLog != "" { - respLogParts = append(respLogParts, "response body", bodyLog) + outLogParts = append(outLogParts, "response body", bodyLog) } } + cfg.respLogFunc(r.Context(), logMessage, outLogParts...) - cfg.respLogFunc(r.Context(), logMessage, respLogParts...) - return resp, err + return resp, nil }) } } +// drainForLog reads body and returns a single-line, truncated copy of it for logging +// along with a replacement left readable by the next hop. A body that cannot be read is +// handed back untouched and logged as empty. +func drainForLog(body io.ReadCloser) (string, io.ReadCloser) { + buf, err := io.ReadAll(body) + if err != nil { + return "", body + } + _ = body.Close() + + bodyLog := string(buf) + if len(bodyLog) > maxBodyLog { + bodyLog = bodyLog[:maxBodyLog] + "..." + } + + return strings.ReplaceAll(bodyLog, "\n", " "), io.NopCloser(bytes.NewReader(buf)) +} + +// headerLog renders headers for a log line, redacting the values of the ones that carry +// credentials - the same set SecretHeader keeps off foreign hosts. +func headerLog(h http.Header) string { + safe := make(http.Header, len(h)) + for k, v := range h { + if credentialHeader(k) { + safe[k] = []string{"REDACTED"} + continue + } + safe[k] = v + } + + log, err := json.Marshal(safe) + if err != nil { + return fmt.Sprintf("%v", safe) + } + + return string(log) +} + func wrapPrefix(prefix string) string { if prefix == "" { return "" diff --git a/caller/middleware/logger_compat_test.go b/caller/middleware/logger_compat_test.go new file mode 100644 index 0000000..feacc7b --- /dev/null +++ b/caller/middleware/logger_compat_test.go @@ -0,0 +1,118 @@ +package middleware_test + +import ( + "bytes" + "encoding/json" + "errors" + "net/http" + "strings" + "testing" + + "github.com/draincloud/callpack/caller" + "github.com/draincloud/callpack/caller/middleware" + "github.com/draincloud/logger" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// The point of LogFunc is that a context-aware key-value logger can be handed over as +// it is. +var ( + _ middleware.LogFunc = logger.Info + _ middleware.LogFunc = logger.Debug + _ middleware.LogFunc = logger.Warn + _ middleware.LogFunc = logger.Error +) + +func logEntries(t *testing.T, buf *bytes.Buffer) []map[string]any { + t.Helper() + + var entries []map[string]any + for _, line := range strings.Split(strings.TrimSpace(buf.String()), "\n") { + if line == "" { + continue + } + var entry map[string]any + require.NoErrorf(t, json.Unmarshal([]byte(line), &entry), "log line is not JSON: %s", line) + entries = append(entries, entry) + } + + return entries +} + +func TestLoggerWithDrainCloudLogger(t *testing.T) { + srv := echoServer(t) + + buf := &bytes.Buffer{} + ctx := logger.NewLoggerContext(t.Context(), logger.WithWriter(buf), logger.WithLevel(logger.LevelInfo)) + + c := caller.New(http.Client{}, middleware.Logger( + middleware.WithLogger(logger.Info), + middleware.WithHeader(), + middleware.WithRequest(), + middleware.WithResponse(logger.Info), + middleware.WithError(logger.Error), + )) + + req := request(t, ctx, http.MethodPost, srv.URL+"/orders", `{"id":1}`) + req.Header.Set("X-Trace-Id", "abc123") + + resp, err := c.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + entries := logEntries(t, buf) + require.Lenf(t, entries, 2, "want a request line and a response line, got %v", entries) + + for _, entry := range entries { + assert.Equal(t, "client", entry["msg"]) + assert.Equal(t, "INFO", entry["level"]) + assert.Equal(t, http.MethodPost, entry["method"]) + // a *url.URL would land here as a nested object under a JSON handler + assert.Equal(t, srv.URL+"/orders", entry["target"]) + } + + reqEntry, respEntry := entries[0], entries[1] + assert.Contains(t, reqEntry["headers"], "X-Trace-Id") + assert.Equal(t, `{"id":1}`, reqEntry["request body"]) + assert.Equal(t, float64(http.StatusOK), respEntry["code"]) + assert.Equal(t, `{"id":1}`, respEntry["response body"]) + assert.NotEmpty(t, respEntry["duration"]) +} + +func TestLoggerWithDrainCloudLoggerOnError(t *testing.T) { + buf := &bytes.Buffer{} + ctx := logger.NewLoggerContext(t.Context(), logger.WithWriter(buf), logger.WithLevel(logger.LevelInfo)) + + boom := errors.New("dial tcp: connection refused") + mw := middleware.Logger( + middleware.WithResponse(logger.Info), + middleware.WithError(logger.Error), + ) + _, _, err := roundTrip(t, mw, failingTransport(boom), request(t, ctx, http.MethodGet, "http://example.com", "")) + require.ErrorIs(t, err, boom) + + entries := logEntries(t, buf) + require.Lenf(t, entries, 1, "want a single outcome line, got %v", entries) + assert.Equal(t, "ERROR", entries[0]["level"]) + assert.Equal(t, boom.Error(), entries[0]["error"]) +} + +// The middleware logs through the logger, so its level gate still decides what gets written. +func TestLoggerWithDrainCloudLoggerLevel(t *testing.T) { + buf := &bytes.Buffer{} + ctx := logger.NewLoggerContext(t.Context(), logger.WithWriter(buf), logger.WithLevel(logger.LevelError)) + + boom := errors.New("boom") + mw := middleware.Logger( + middleware.WithLogger(logger.Info), + middleware.WithResponse(logger.Info), + middleware.WithError(logger.Error), + ) + _, _, err := roundTrip(t, mw, failingTransport(boom), request(t, ctx, http.MethodGet, "http://example.com", "")) + require.ErrorIs(t, err, boom) + + entries := logEntries(t, buf) + require.Lenf(t, entries, 1, "want only the error line past a LevelError gate, got %v", entries) + assert.Equal(t, "ERROR", entries[0]["level"]) +} diff --git a/caller/middleware/logger_test.go b/caller/middleware/logger_test.go index 37961f0..968b457 100644 --- a/caller/middleware/logger_test.go +++ b/caller/middleware/logger_test.go @@ -1,18 +1,362 @@ -package middleware +package middleware_test import ( - "log/slog" + "context" + "errors" + "io" "net/http" + "net/http/httptest" + "strings" + "sync" "testing" + + "github.com/draincloud/callpack/caller/middleware" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func TestLogger(t *testing.T) { - r := &http.Request{ - Header: http.Header{ - "h1": []string{"1", "2", "3"}, - "h2": []string{"qwe"}, - }, +type logRecorder struct { + mu sync.Mutex + lines []logLine +} + +type logLine struct { + ctx context.Context //nolint:containedctx // the ctx handed to the LogFunc is under test + msg string + args []any +} + +func (r *logRecorder) log(ctx context.Context, msg string, args ...any) { + r.mu.Lock() + defer r.mu.Unlock() + r.lines = append(r.lines, logLine{ctx: ctx, msg: msg, args: args}) +} + +func (r *logRecorder) all() []logLine { + r.mu.Lock() + defer r.mu.Unlock() + return append([]logLine{}, r.lines...) +} + +func (r *logRecorder) only(t *testing.T) logLine { + t.Helper() + + lines := r.all() + require.Lenf(t, lines, 1, "want exactly one log line, got %v", lines) + + return lines[0] +} + +func (r *logRecorder) onlyAttrs(t *testing.T) map[string]any { + t.Helper() + return r.only(t).attrs(t) +} + +func (l logLine) attrs(t *testing.T) map[string]any { + t.Helper() + + require.Zerof(t, len(l.args)%2, "log args are not key-value pairs: %v", l.args) + + attrs := make(map[string]any, len(l.args)/2) + for i := 0; i < len(l.args); i += 2 { + key, ok := l.args[i].(string) + require.Truef(t, ok, "log arg %d is not a string key, got %T: %v", i, l.args[i], l.args) + attrs[key] = l.args[i+1] } - slog.Info("123", "headers", r.Header) + return attrs +} + +func echoServer(t *testing.T) *httptest.Server { + t.Helper() + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + if len(body) == 0 { + body = []byte("pong") + } + _, _ = w.Write(body) + })) + t.Cleanup(srv.Close) + + return srv +} + +func failingTransport(err error) http.RoundTripper { + return middleware.RoundTripperFunc(func(*http.Request) (*http.Response, error) { + return nil, err + }) +} + +func request(t *testing.T, ctx context.Context, method, target, body string) *http.Request { + t.Helper() + + var r io.Reader + if body != "" { + r = strings.NewReader(body) + } + + req, err := http.NewRequestWithContext(ctx, method, target, r) + require.NoError(t, err) + + return req +} + +// roundTrip also returns the body the caller is left with, which the middleware must +// not have consumed. +func roundTrip(t *testing.T, mw middleware.RoundTripperHandler, next http.RoundTripper, req *http.Request) (*http.Response, string, error) { + t.Helper() + + resp, err := mw(next).RoundTrip(req) + if resp == nil { + return nil, "", err + } + defer resp.Body.Close() + + body, readErr := io.ReadAll(resp.Body) + require.NoError(t, readErr) + + return resp, string(body), err +} + +func TestLoggerRequestLine(t *testing.T) { + rec := &logRecorder{} + srv := echoServer(t) + + mw := middleware.Logger(middleware.WithLogger(rec.log)) + _, _, err := roundTrip(t, mw, http.DefaultTransport, request(t, t.Context(), http.MethodGet, srv.URL+"/ping", "")) + require.NoError(t, err) + + line := rec.only(t) + assert.Equal(t, "client", line.msg) + + attrs := line.attrs(t) + assert.Equal(t, http.MethodGet, attrs["method"]) + assert.Equal(t, srv.URL+"/ping", attrs["target"]) + assert.NotContains(t, attrs, "headers") + assert.NotContains(t, attrs, "request body") +} + +func TestLoggerDiscardsRequestLineWithoutLogger(t *testing.T) { + srv := echoServer(t) + + var called bool + next := middleware.RoundTripperFunc(func(r *http.Request) (*http.Response, error) { + called = true + return http.DefaultTransport.RoundTrip(r) + }) + + mw := middleware.Logger(middleware.WithRequest(), middleware.WithHeader()) + _, body, err := roundTrip(t, mw, next, request(t, t.Context(), http.MethodPost, srv.URL, "payload")) + require.NoError(t, err) + assert.True(t, called, "next transport was not called") + assert.Equal(t, "payload", body) +} + +func TestLoggerWithHeaderRedactsCredentials(t *testing.T) { + rec := &logRecorder{} + srv := echoServer(t) + + req := request(t, t.Context(), http.MethodGet, srv.URL, "") + req.Header.Set("X-Trace-Id", "abc123") + req.Header.Set("Authorization", "Bearer super-secret") + req.Header.Set("Cookie", "session=super-secret") + + mw := middleware.Logger(middleware.WithLogger(rec.log), middleware.WithHeader()) + _, _, err := roundTrip(t, mw, http.DefaultTransport, req) + require.NoError(t, err) + + headers, ok := rec.onlyAttrs(t)["headers"].(string) + require.True(t, ok, "headers attribute is not a string") + assert.Contains(t, headers, `"X-Trace-Id":["abc123"]`) + assert.NotContains(t, headers, "super-secret") + assert.Contains(t, headers, "Authorization") + assert.Contains(t, headers, "Cookie") +} + +func TestLoggerWithHeaderSkipsEmptyHeaders(t *testing.T) { + rec := &logRecorder{} + + next := middleware.RoundTripperFunc(func(*http.Request) (*http.Response, error) { + return &http.Response{StatusCode: http.StatusOK, Body: http.NoBody}, nil + }) + + req := request(t, t.Context(), http.MethodGet, "http://example.com", "") + req.Header = http.Header{} + + mw := middleware.Logger(middleware.WithLogger(rec.log), middleware.WithHeader()) + _, _, err := roundTrip(t, mw, next, req) + require.NoError(t, err) + + assert.NotContains(t, rec.onlyAttrs(t), "headers") +} + +func TestLoggerWithRequestLeavesBodyReadable(t *testing.T) { + rec := &logRecorder{} + srv := echoServer(t) + + mw := middleware.Logger(middleware.WithLogger(rec.log), middleware.WithRequest()) + _, echoed, err := roundTrip(t, mw, http.DefaultTransport, request(t, t.Context(), http.MethodPost, srv.URL, `{"id":1}`)) + require.NoError(t, err) + + assert.Equal(t, `{"id":1}`, rec.onlyAttrs(t)["request body"]) + assert.Equal(t, `{"id":1}`, echoed, "server saw a body the logger had consumed") +} + +func TestLoggerTruncatesAndFlattensBody(t *testing.T) { + rec := &logRecorder{} + srv := echoServer(t) + + body := "head\nline\n" + strings.Repeat("x", 2048) + mw := middleware.Logger(middleware.WithLogger(rec.log), middleware.WithRequest()) + _, echoed, err := roundTrip(t, mw, http.DefaultTransport, request(t, t.Context(), http.MethodPost, srv.URL, body)) + require.NoError(t, err) + + logged, ok := rec.onlyAttrs(t)["request body"].(string) + require.True(t, ok, "request body attribute is not a string") + assert.Len(t, logged, 1024+len("...")) + assert.True(t, strings.HasSuffix(logged, "..."), "logged body = %q", logged) + assert.NotContains(t, logged, "\n") + assert.True(t, strings.HasPrefix(logged, "head line "), "logged body = %q", logged) + assert.Equal(t, body, echoed, "truncating for the log truncated what was sent") +} + +func TestLoggerWithResponse(t *testing.T) { + rec := &logRecorder{} + srv := echoServer(t) + + mw := middleware.Logger(middleware.WithResponse(rec.log)) + resp, body, err := roundTrip(t, mw, http.DefaultTransport, request(t, t.Context(), http.MethodGet, srv.URL, "")) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + + attrs := rec.onlyAttrs(t) + assert.Equal(t, http.StatusOK, attrs["code"]) + assert.Equal(t, "pong", attrs["response body"]) + assert.NotEmpty(t, attrs["duration"]) + assert.NotContains(t, attrs, "error") + assert.Equal(t, "pong", body, "caller was left a body the logger had consumed") +} + +func TestLoggerRequestAndResponseAreSeparateLines(t *testing.T) { + reqRec, respRec := &logRecorder{}, &logRecorder{} + srv := echoServer(t) + + mw := middleware.Logger( + middleware.WithLogger(reqRec.log), + middleware.WithResponse(respRec.log), + ) + _, _, err := roundTrip(t, mw, http.DefaultTransport, request(t, t.Context(), http.MethodGet, srv.URL, "")) + require.NoError(t, err) + + assert.NotContains(t, reqRec.onlyAttrs(t), "code") + assert.Contains(t, respRec.onlyAttrs(t), "code") +} + +func TestLoggerTransportErrorGoesToErrorFunc(t *testing.T) { + reqRec, respRec, errRec := &logRecorder{}, &logRecorder{}, &logRecorder{} + boom := errors.New("dial tcp: connection refused") + + mw := middleware.Logger( + middleware.WithLogger(reqRec.log), + middleware.WithResponse(respRec.log), + middleware.WithError(errRec.log), + ) + resp, _, err := roundTrip(t, mw, failingTransport(boom), request(t, t.Context(), http.MethodGet, "http://example.com/x", "")) + require.ErrorIs(t, err, boom) + require.Nil(t, resp) + + attrs := errRec.onlyAttrs(t) + assert.Equal(t, boom.Error(), attrs["error"]) + assert.Equal(t, "http://example.com/x", attrs["target"]) + assert.Contains(t, attrs, "duration") + assert.NotContains(t, attrs, "code") + assert.Empty(t, respRec.all(), "response func was called on a failed trip") +} + +func TestLoggerTransportErrorWithoutErrorOption(t *testing.T) { + respRec := &logRecorder{} + boom := errors.New("boom") + + mw := middleware.Logger(middleware.WithResponse(respRec.log)) + _, _, err := roundTrip(t, mw, failingTransport(boom), request(t, t.Context(), http.MethodGet, "http://example.com", "")) + require.ErrorIs(t, err, boom) + + assert.Equal(t, boom.Error(), respRec.onlyAttrs(t)["error"]) +} + +func TestLoggerErrorOptionAloneIsQuietOnSuccess(t *testing.T) { + errRec := &logRecorder{} + srv := echoServer(t) + + mw := middleware.Logger(middleware.WithError(errRec.log)) + resp, body, err := roundTrip(t, mw, http.DefaultTransport, request(t, t.Context(), http.MethodGet, srv.URL, "")) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "pong", body) + assert.Empty(t, errRec.all(), "error func was called on a successful trip") +} + +func TestLoggerSkipsRoundTripBookkeepingWhenNothingIsLogged(t *testing.T) { + rec := &logRecorder{} + srv := echoServer(t) + + mw := middleware.Logger(middleware.WithLogger(rec.log)) + resp, body, err := roundTrip(t, mw, http.DefaultTransport, request(t, t.Context(), http.MethodGet, srv.URL, "")) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "pong", body) + assert.NotContains(t, rec.onlyAttrs(t), "code") +} + +func TestLoggerWithPrefix(t *testing.T) { + rec := &logRecorder{} + srv := echoServer(t) + + mw := middleware.Logger(middleware.WithLogger(rec.log), middleware.WithPrefix("[consul]")) + _, _, err := roundTrip(t, mw, http.DefaultTransport, request(t, t.Context(), http.MethodGet, srv.URL, "")) + require.NoError(t, err) + + assert.Equal(t, "[consul] client", rec.only(t).msg) +} + +func TestLoggerWithLogArgsOnEveryLine(t *testing.T) { + reqRec, respRec := &logRecorder{}, &logRecorder{} + srv := echoServer(t) + + mw := middleware.Logger( + middleware.WithLogger(reqRec.log), + middleware.WithResponse(respRec.log), + middleware.WithLogArgs("service", "billing"), + ) + _, _, err := roundTrip(t, mw, http.DefaultTransport, request(t, t.Context(), http.MethodGet, srv.URL, "")) + require.NoError(t, err) + + for name, rec := range map[string]*logRecorder{"request": reqRec, "response": respRec} { + assert.Equal(t, "billing", rec.onlyAttrs(t)["service"], "%s line", name) + } +} + +type ctxKey struct{} + +func TestLoggerPassesRequestContext(t *testing.T) { + reqRec, respRec := &logRecorder{}, &logRecorder{} + srv := echoServer(t) + + ctx := context.WithValue(t.Context(), ctxKey{}, "traced") + + mw := middleware.Logger( + middleware.WithLogger(reqRec.log), + middleware.WithResponse(respRec.log), + ) + _, _, err := roundTrip(t, mw, http.DefaultTransport, request(t, ctx, http.MethodGet, srv.URL, "")) + require.NoError(t, err) + + for name, rec := range map[string]*logRecorder{"request": reqRec, "response": respRec} { + assert.Equal(t, "traced", rec.only(t).ctx.Value(ctxKey{}), "%s line", name) + } } diff --git a/go.work b/go.work new file mode 100644 index 0000000..0ce97a0 --- /dev/null +++ b/go.work @@ -0,0 +1,7 @@ +go 1.25.0 + +use ( + ./caller + ./integration + ./registry +) diff --git a/integration/discovery_test.go b/integration/discovery_test.go new file mode 100644 index 0000000..d9dd4ce --- /dev/null +++ b/integration/discovery_test.go @@ -0,0 +1,183 @@ +// Package integration exercises registry and the consul caller middleware against one +// another: what registry writes to the catalog is what the middleware has to be able to +// resolve. Neither module depends on the other, so the round trip has no other home. +package integration + +import ( + "context" + "encoding/json" + "fmt" + "io" + "log/slog" + "net/http" + "net/http/httptest" + "strings" + "sync" + "testing" + "time" + + "github.com/draincloud/callpack/caller" + "github.com/draincloud/callpack/caller/middleware/consul" + "github.com/draincloud/callpack/registry" + "github.com/hashicorp/consul/api" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// agent is a consul agent that serves the registration side registry writes to and the +// health side the middleware reads from. +type agent struct { + mu sync.Mutex + services map[string]*api.AgentServiceRegistration + checks map[string]string +} + +func newAgent(t *testing.T) *api.Client { + t.Helper() + + a := &agent{ + services: map[string]*api.AgentServiceRegistration{}, + checks: map[string]string{}, + } + + mux := http.NewServeMux() + mux.HandleFunc("PUT /v1/agent/service/register", a.register) + mux.HandleFunc("PUT /v1/agent/service/deregister/{id}", a.deregister) + mux.HandleFunc("PUT /v1/agent/check/update/{id}", a.update) + mux.HandleFunc("GET /v1/health/service/{name}", a.health) + + srv := httptest.NewServer(mux) + t.Cleanup(srv.Close) + + config := api.DefaultConfig() + config.Address = strings.TrimPrefix(srv.URL, "http://") + client, err := api.NewClient(config) + require.NoError(t, err) + + return client +} + +func (a *agent) register(w http.ResponseWriter, r *http.Request) { + var svc api.AgentServiceRegistration + if err := json.NewDecoder(r.Body).Decode(&svc); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + a.mu.Lock() + defer a.mu.Unlock() + a.services[svc.ID] = &svc + a.checks[svc.Check.CheckID] = svc.Check.Status +} + +func (a *agent) deregister(_ http.ResponseWriter, r *http.Request) { + a.mu.Lock() + defer a.mu.Unlock() + delete(a.services, r.PathValue("id")) +} + +func (a *agent) update(w http.ResponseWriter, r *http.Request) { + var update struct{ Status, Output string } + if err := json.NewDecoder(r.Body).Decode(&update); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + a.mu.Lock() + defer a.mu.Unlock() + a.checks[r.PathValue("id")] = update.Status +} + +func (a *agent) health(w http.ResponseWriter, r *http.Request) { + name := r.PathValue("name") + passingOnly := r.URL.Query().Get("passing") != "" + + a.mu.Lock() + entries := []*api.ServiceEntry{} + for _, svc := range a.services { + if svc.Name != name || (passingOnly && a.checks[svc.Check.CheckID] != api.HealthPassing) { + continue + } + entries = append(entries, &api.ServiceEntry{ + Node: &api.Node{Node: "node", Address: "10.0.0.1"}, + Service: &api.AgentService{ID: svc.ID, Service: svc.Name, Address: svc.Address, Port: svc.Port}, + }) + } + a.mu.Unlock() + + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(entries) +} + +func register(t *testing.T, client *api.Client, svc registry.Service) { + t.Helper() + + if svc.Logger == nil { + svc.Logger = slog.New(slog.NewTextHandler(io.Discard, nil)) + } + reg, err := registry.Register(t.Context(), client, svc) + require.NoErrorf(t, err, "register %q", svc.Name) + t.Cleanup(func() { _ = reg.Close() }) +} + +func split(t *testing.T, rawURL string) (host string, port int) { + t.Helper() + + host, rawPort, _ := strings.Cut(strings.TrimPrefix(rawURL, "http://"), ":") + _, err := fmt.Sscanf(rawPort, "%d", &port) + require.NoErrorf(t, err, "port of %q", rawURL) + + return host, port +} + +func TestRegisteredServiceIsDiscoverableThroughTheMiddleware(t *testing.T) { + client := newAgent(t) + + replicas := map[string]bool{} + var mu sync.Mutex + for _, name := range []string{"one", "two"} { + backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + mu.Lock() + replicas[name] = true + mu.Unlock() + fmt.Fprintf(w, "served by %s, host %s", name, r.Host) + })) + t.Cleanup(backend.Close) + + host, port := split(t, backend.URL) + register(t, client, registry.Service{Name: "api", Address: host, Port: port}) + } + + call := caller.New(http.Client{}, consul.Resolve(client, 0)) + for i := range 2 { + req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, "http://api/things", nil) + require.NoError(t, err) + + resp, err := call.Do(req) + require.NoErrorf(t, err, "request %d", i) + + body, _ := io.ReadAll(resp.Body) + resp.Body.Close() + assert.Contains(t, string(body), "host api", "want the service name as the Host header") + } + + mu.Lock() + defer mu.Unlock() + assert.Len(t, replicas, 2, "want both replicas load balanced across, reached %v", replicas) +} + +func TestUnhealthyServiceIsNotDiscoverable(t *testing.T) { + client := newAgent(t) + + register(t, client, registry.Service{ + Name: "api", Address: "10.1.2.3", Port: 8080, TTL: 40 * time.Millisecond, + Health: func(context.Context) error { return fmt.Errorf("not ready") }, + }) + + call := caller.New(http.Client{}, consul.Resolve(client, time.Millisecond)) + req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, "http://api/things", nil) + require.NoError(t, err) + + _, err = call.Do(req) + assert.Error(t, err, "the unhealthy replica was routed to") +} diff --git a/integration/go.mod b/integration/go.mod new file mode 100644 index 0000000..1c519fc --- /dev/null +++ b/integration/go.mod @@ -0,0 +1,36 @@ +module github.com/draincloud/callpack/integration + +go 1.25.0 + +// Never published: the round trip it tests only exists inside this repo, so it always +// builds against the working tree rather than a released version of either module. +replace github.com/draincloud/callpack/caller => ../caller + +replace github.com/draincloud/callpack/registry => ../registry + +require ( + github.com/draincloud/callpack/caller v0.0.0 + github.com/draincloud/callpack/registry v0.0.0 + github.com/hashicorp/consul/api v1.32.1 + github.com/stretchr/testify v1.12.1 +) + +require ( + github.com/armon/go-metrics v0.4.1 // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-hclog v1.5.0 // indirect + github.com/hashicorp/go-immutable-radix v1.3.1 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-rootcerts v1.0.2 // indirect + github.com/hashicorp/golang-lru v0.5.4 // indirect + github.com/hashicorp/serf v0.10.1 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect + golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect + golang.org/x/sys v0.31.0 // indirect +) diff --git a/integration/go.sum b/integration/go.sum new file mode 100644 index 0000000..4bcf8c3 --- /dev/null +++ b/integration/go.sum @@ -0,0 +1,224 @@ +github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA= +github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/draincloud/logger v0.0.4 h1:BEbNyy8AkuqQy60uST7O9IgW8ofl1zI6iN4tygFTwAg= +github.com/draincloud/logger v0.0.4/go.mod h1:t/vRd80p4+3cQg7HlmRCm5/GSTHfTWDC0WWANdtKYMw= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= +github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/hashicorp/consul/api v1.32.1 h1:0+osr/3t/aZNAdJX558crU3PEjVrG4x6715aZHRgceE= +github.com/hashicorp/consul/api v1.32.1/go.mod h1:mXUWLnxftwTmDv4W3lzxYCPD199iNLLUyLfLGFJbtl4= +github.com/hashicorp/consul/sdk v0.16.1 h1:V8TxTnImoPD5cj0U9Spl0TUxcytjcbbJeADFF07KdHg= +github.com/hashicorp/consul/sdk v0.16.1/go.mod h1:fSXvwxB2hmh1FMZCNl6PwX0Q/1wdWtHJcZ7Ea5tns0s= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= +github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-msgpack v0.5.5 h1:i9R9JSrqIz0QVLz3sz+i3YJdT7TTSLcfLLzJi9aZTuI= +github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= +github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI= +github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= +github.com/hashicorp/memberlist v0.5.0 h1:EtYPN8DpAURiapus508I4n9CzHs2W+8NZGbmmR/prTM= +github.com/hashicorp/memberlist v0.5.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0= +github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY= +github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= +github.com/miekg/dns v1.1.41 h1:WMszZWJG0XmzbK9FEmzH2TVcqYzFesusSIB41b8KHxY= +github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= +github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= +github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go.mod b/registry/go.mod similarity index 95% rename from go.mod rename to registry/go.mod index 426bb06..23aa560 100644 --- a/go.mod +++ b/registry/go.mod @@ -1,4 +1,4 @@ -module github.com/draincloud/callpack +module github.com/draincloud/callpack/registry go 1.25.0 diff --git a/go.sum b/registry/go.sum similarity index 100% rename from go.sum rename to registry/go.sum diff --git a/registry/registry_test.go b/registry/registry_test.go index e5593cd..0003bb2 100644 --- a/registry/registry_test.go +++ b/registry/registry_test.go @@ -13,8 +13,6 @@ import ( "testing" "time" - "github.com/draincloud/callpack/caller" - "github.com/draincloud/callpack/caller/middleware/consul" "github.com/draincloud/callpack/registry" "github.com/hashicorp/consul/api" ) @@ -342,74 +340,3 @@ func TestCloseTakesTheInstanceOutOfTheCatalog(t *testing.T) { t.Errorf("second close: %v, want it to be a no-op", err) } } - -func TestRegisteredServiceIsDiscoverableThroughTheMiddleware(t *testing.T) { - _, client := newAgent(t) - - replicas := map[string]bool{} - var mu sync.Mutex - for _, name := range []string{"one", "two"} { - backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - mu.Lock() - replicas[name] = true - mu.Unlock() - fmt.Fprintf(w, "served by %s, host %s", name, r.Host) - })) - t.Cleanup(backend.Close) - - host, port := split(t, backend.URL) - register(t, client, registry.Service{Name: "api", Address: host, Port: port}) - } - - call := caller.New(http.Client{}, consul.Resolve(client, 0)) - for i := range 2 { - req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, "http://api/things", nil) - if err != nil { - t.Fatalf("request: %v", err) - } - resp, err := call.Do(req) - if err != nil { - t.Fatalf("request %d: %v", i, err) - } - body, _ := io.ReadAll(resp.Body) - resp.Body.Close() - - if !strings.Contains(string(body), "host api") { - t.Errorf("backend saw %q, want the service name as the Host header", body) - } - } - - mu.Lock() - defer mu.Unlock() - if len(replicas) != 2 { - t.Errorf("only %v were reached, want both replicas load balanced across", replicas) - } -} -func TestUnhealthyServiceIsNotDiscoverable(t *testing.T) { - _, client := newAgent(t) - - register(t, client, registry.Service{ - Name: "api", Address: "10.1.2.3", Port: 8080, TTL: 40 * time.Millisecond, - Health: func(context.Context) error { return fmt.Errorf("not ready") }, - }) - - call := caller.New(http.Client{}, consul.Resolve(client, time.Millisecond)) - req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, "http://api/things", nil) - if err != nil { - t.Fatalf("request: %v", err) - } - if _, err := call.Do(req); err == nil { - t.Fatal("the unhealthy replica was routed to") - } -} - -func split(t *testing.T, rawURL string) (host string, port int) { - t.Helper() - - var err error - host, rawPort, _ := strings.Cut(strings.TrimPrefix(rawURL, "http://"), ":") - if _, err = fmt.Sscanf(rawPort, "%d", &port); err != nil { - t.Fatalf("port of %q: %v", rawURL, err) - } - return host, port -}