mirror of
https://github.com/hanzokv/go.git
synced 2026-09-24 00:31:51 +00:00
merge: upstream/master (preserve Hanzo brand, 100 commits)
This commit is contained in:
@@ -3,45 +3,70 @@ description: 'Runs go-redis tests against different Redis versions and configura
|
||||
inputs:
|
||||
go-version:
|
||||
description: 'Go version to use for running tests'
|
||||
default: '1.23'
|
||||
default: '1.25'
|
||||
redis-version:
|
||||
description: 'Redis version to test against'
|
||||
required: true
|
||||
required: false
|
||||
repository:
|
||||
description: 'Repository to checkout'
|
||||
required: false
|
||||
ref:
|
||||
description: 'Branch to checkout'
|
||||
required: false
|
||||
client-libs-test-image-tag:
|
||||
description: 'Client libs test image tag'
|
||||
required: false
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: ${{ inputs.repository }}
|
||||
ref: ${{ inputs.ref }}
|
||||
|
||||
- name: Set up ${{ inputs.go-version }}
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ inputs.go-version }}
|
||||
|
||||
- name: Setup Test environment
|
||||
env:
|
||||
REDIS_VERSION: ${{ inputs.redis-version }}
|
||||
CLIENT_LIBS_TEST_IMAGE_TAG: ${{ inputs.client-libs-test-image-tag }}
|
||||
run: |
|
||||
set -e
|
||||
redis_version_np=$(echo "$REDIS_VERSION" | grep -oP '^\d+.\d+')
|
||||
|
||||
# Mapping of redis version to redis testing containers
|
||||
declare -A redis_version_mapping=(
|
||||
["8.4.x"]="8.4.0"
|
||||
["8.2.x"]="8.2.1-pre"
|
||||
["8.0.x"]="8.0.2"
|
||||
)
|
||||
if [[ -n "$REDIS_VERSION" ]]; then
|
||||
redis_version_np=$(echo "$REDIS_VERSION" | grep -oP '^\d+.\d+')
|
||||
echo "REDIS_VERSION=${redis_version_np}" >> "$GITHUB_ENV"
|
||||
fi
|
||||
|
||||
if [[ -v redis_version_mapping[$REDIS_VERSION] ]]; then
|
||||
echo "REDIS_VERSION=${redis_version_np}" >> $GITHUB_ENV
|
||||
echo "REDIS_IMAGE=redis:${REDIS_VERSION}" >> $GITHUB_ENV
|
||||
echo "CLIENT_LIBS_TEST_IMAGE=redislabs/client-libs-test:${redis_version_mapping[$REDIS_VERSION]}" >> $GITHUB_ENV
|
||||
else
|
||||
echo "Version not found in the mapping."
|
||||
exit 1
|
||||
if [[ -n "$CLIENT_LIBS_TEST_IMAGE_TAG" ]]; then
|
||||
echo "CLIENT_LIBS_TEST_IMAGE=redislabs/client-libs-test:${CLIENT_LIBS_TEST_IMAGE_TAG}" >> "$GITHUB_ENV"
|
||||
elif [[ -n "$REDIS_VERSION" ]]; then
|
||||
# Mapping of redis version to redis testing containers
|
||||
declare -A redis_version_mapping=(
|
||||
["8.8.x"]="8.8.0"
|
||||
["8.6.x"]="8.6.1"
|
||||
["8.4.x"]="8.4.0"
|
||||
["8.2.x"]="8.2.1-pre"
|
||||
["8.0.x"]="8.0.2"
|
||||
)
|
||||
|
||||
if [[ -v redis_version_mapping[$REDIS_VERSION] ]]; then
|
||||
echo "CLIENT_LIBS_TEST_IMAGE=redislabs/client-libs-test:${redis_version_mapping[$REDIS_VERSION]}" >> "$GITHUB_ENV"
|
||||
else
|
||||
echo "Version not found in the mapping and CLIENT_LIBS_TEST_IMAGE_TAG was not provided."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
sleep 10 # wait for redis to start
|
||||
shell: bash
|
||||
- name: Set up Docker Compose environment with redis ${{ inputs.redis-version }}
|
||||
- name: Set up Docker Compose environment
|
||||
run: |
|
||||
make docker.start
|
||||
sleep 5
|
||||
shell: bash
|
||||
- name: Run tests
|
||||
env:
|
||||
@@ -49,4 +74,4 @@ runs:
|
||||
RE_CLUSTER: "false"
|
||||
run: |
|
||||
make test.ci
|
||||
shell: bash
|
||||
shell: bash
|
||||
|
||||
@@ -77,3 +77,4 @@ entraid
|
||||
MiB
|
||||
KiB
|
||||
oldstable
|
||||
backoff
|
||||
|
||||
@@ -4,7 +4,7 @@ on:
|
||||
push:
|
||||
branches: [master, v9, 'v9.*']
|
||||
pull_request:
|
||||
branches: [master, v9, v9.7, v9.8, 'ndyakov/*', 'ofekshenawa/*', 'htemelski-redis/*', 'ce/*']
|
||||
branches: [master, v9, v9.7, v9.8, 'ndyakov/**', 'ofekshenawa/**', 'ce/**']
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -18,12 +18,13 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
redis-version:
|
||||
- "8.8.x" # Redis CE 8.8
|
||||
- "8.6.x" # Redis CE 8.6
|
||||
- "8.4.x" # Redis CE 8.4
|
||||
- "8.2.x" # Redis CE 8.2
|
||||
- "8.0.x" # Redis CE 8.0
|
||||
go-version:
|
||||
- "1.21.x"
|
||||
- "1.23.x"
|
||||
- "1.25.x"
|
||||
- oldstable
|
||||
- stable
|
||||
|
||||
@@ -46,6 +47,8 @@ jobs:
|
||||
|
||||
# Mapping of redis version to redis testing containers
|
||||
declare -A redis_version_mapping=(
|
||||
["8.8.x"]="8.8.0"
|
||||
["8.6.x"]="8.6.1"
|
||||
["8.4.x"]="8.4.0"
|
||||
["8.2.x"]="8.2.1-pre"
|
||||
["8.0.x"]="8.0.2"
|
||||
@@ -76,12 +79,13 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
redis-version:
|
||||
- "8.8.x" # Redis CE 8.8
|
||||
- "8.6.x" # Redis CE 8.6
|
||||
- "8.4.x" # Redis CE 8.4
|
||||
- "8.2.x" # Redis CE 8.2
|
||||
- "8.0.x" # Redis CE 8.0
|
||||
go-version:
|
||||
- "1.21.x"
|
||||
- "1.23.x"
|
||||
- "1.25.x"
|
||||
- oldstable
|
||||
- stable
|
||||
|
||||
@@ -96,7 +100,7 @@ jobs:
|
||||
redis-version: ${{ matrix.redis-version }}
|
||||
|
||||
- name: Upload to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
uses: codecov/codecov-action@v6
|
||||
with:
|
||||
files: coverage.txt
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
|
||||
services:
|
||||
redis-stack:
|
||||
image: redislabs/client-libs-test:8.4.0
|
||||
image: redislabs/client-libs-test:8.8.0
|
||||
env:
|
||||
TLS_ENABLED: no
|
||||
REDIS_CLUSTER: no
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
go-version: ["1.24"]
|
||||
go-version: ["1.25"]
|
||||
|
||||
steps:
|
||||
- name: Set up ${{ matrix.go-version }}
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: golangci-lint
|
||||
uses: golangci/[email protected].0
|
||||
uses: golangci/[email protected].1
|
||||
with:
|
||||
verify: true
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
# .github/workflows/govulncheck.yml
|
||||
name: govulncheck
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [master, v9, v9.7, v9.8, "ndyakov/**", "ofekshenawa/**", "ce/**"]
|
||||
push:
|
||||
branches: [master, v9, "v9.*"]
|
||||
schedule:
|
||||
- cron: "0 6 * * 1"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: govulncheck-${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
govulncheck:
|
||||
name: govulncheck
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.26.x"
|
||||
cache: true
|
||||
|
||||
- name: Install govulncheck
|
||||
run: go install golang.org/x/vuln/cmd/[email protected]
|
||||
|
||||
- name: Run govulncheck (all modules)
|
||||
shell: bash
|
||||
run: |
|
||||
set -u -o pipefail
|
||||
|
||||
status=0
|
||||
found=0
|
||||
|
||||
while IFS= read -r gomod; do
|
||||
found=1
|
||||
mod=$(dirname "$gomod")
|
||||
echo "Scanning module: $mod"
|
||||
|
||||
if ! (
|
||||
cd "$mod" && govulncheck ./...
|
||||
); then
|
||||
status=1
|
||||
fi
|
||||
done < <(
|
||||
find . \
|
||||
-type f -name go.mod \
|
||||
-not -path '*/vendor/*' \
|
||||
-not -path '*/.git/*' \
|
||||
| sort -u
|
||||
)
|
||||
|
||||
if [ "$found" -eq 0 ]; then
|
||||
echo "No Go modules found to scan."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exit "$status"
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Drafts your next Release notes as Pull Requests are merged into "master"
|
||||
- uses: release-drafter/release-drafter@v6
|
||||
- uses: release-drafter/release-drafter@v7
|
||||
with:
|
||||
# (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml
|
||||
config-name: release-drafter-config.yml
|
||||
|
||||
@@ -8,7 +8,7 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
- name: Check Spelling
|
||||
uses: rojopolis/spellcheck-github-actions@0.57.0
|
||||
uses: rojopolis/spellcheck-github-actions@0.60.0
|
||||
with:
|
||||
config_path: .github/spellcheck-settings.yml
|
||||
task_name: Markdown
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
name: E2E Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master, v9, 'v9.*']
|
||||
pull_request:
|
||||
branches: [master, v9, v9.7, v9.8, 'ndyakov/**', 'ofekshenawa/**', 'ce/**']
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test-e2e-mock:
|
||||
name: E2E Tests (Mock Proxy)
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
go-version:
|
||||
- stable
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Go ${{ matrix.go-version }}
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
- name: Start Docker services for E2E tests
|
||||
run: make docker.e2e.start
|
||||
|
||||
- name: Wait for services to be ready
|
||||
run: |
|
||||
echo "Waiting for Redis to be ready..."
|
||||
timeout 30 bash -c 'until docker exec redis-standalone redis-cli ping 2>/dev/null; do sleep 1; done'
|
||||
echo "Waiting for cae-resp-proxy to be ready..."
|
||||
timeout 30 bash -c 'until curl -s http://localhost:18100/stats > /dev/null; do sleep 1; done'
|
||||
echo "All services are ready!"
|
||||
|
||||
- name: Run E2E tests with mock proxy
|
||||
env:
|
||||
E2E_SCENARIO_TESTS: "true"
|
||||
run: |
|
||||
go test -v ./maintnotifications/e2e/ -timeout 30m -race
|
||||
continue-on-error: false
|
||||
|
||||
- name: Stop Docker services
|
||||
if: always()
|
||||
run: make docker.e2e.stop
|
||||
|
||||
- name: Show Docker logs on failure
|
||||
if: failure()
|
||||
run: |
|
||||
echo "=== Redis logs ==="
|
||||
docker logs redis-standalone 2>&1 | tail -100
|
||||
echo "=== cae-resp-proxy logs ==="
|
||||
docker logs cae-resp-proxy 2>&1 | tail -100
|
||||
echo "=== proxy-fault-injector logs ==="
|
||||
docker logs proxy-fault-injector 2>&1 | tail -100
|
||||
|
||||
@@ -10,12 +10,13 @@ permissions:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: false # Temporarily disabled, should be enabled when we have a current and stable RE build
|
||||
name: build
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
go-version: [1.24.x]
|
||||
go-version: [1.25.x]
|
||||
re-build: ["7.4.2-54"]
|
||||
|
||||
steps:
|
||||
@@ -50,8 +51,11 @@ jobs:
|
||||
REDIS_VERSION: "7.4"
|
||||
run: |
|
||||
go test \
|
||||
-skip="^TestTLS" \
|
||||
--ginkgo.skip-file="ring_test.go" \
|
||||
--ginkgo.skip-file="sentinel_test.go" \
|
||||
--ginkgo.skip-file="osscluster_test.go" \
|
||||
--ginkgo.skip-file="pubsub_test.go" \
|
||||
--ginkgo.skip-file="tls_test.go" \
|
||||
--ginkgo.skip-file="tls_cluster_test.go" \
|
||||
--ginkgo.label-filter='!NonRedisEnterprise'
|
||||
|
||||
+5
-1
@@ -10,6 +10,10 @@ coverage.txt
|
||||
.vscode
|
||||
tmp/*
|
||||
*.test
|
||||
|
||||
extra/redisotel-native/metrics-collector-app/
|
||||
# maintenanceNotifications upgrade documentation (temporary)
|
||||
maintenanceNotifications/docs/
|
||||
|
||||
# Docker-generated files (TLS certificates, cluster data, etc.)
|
||||
dockers/*/tls/
|
||||
dockers/osscluster-tls/
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
GO_MOD_DIRS := $(shell find . -type f -name 'go.mod' -exec dirname {} \; | sort)
|
||||
REDIS_VERSION ?= 8.4
|
||||
REDIS_VERSION ?= 8.8
|
||||
RE_CLUSTER ?= false
|
||||
RCE_DOCKER ?= true
|
||||
CLIENT_LIBS_TEST_IMAGE ?= redislabs/client-libs-test:8.4.0
|
||||
CLIENT_LIBS_TEST_IMAGE ?= redislabs/client-libs-test:8.8.0
|
||||
|
||||
docker.start:
|
||||
export RE_CLUSTER=$(RE_CLUSTER) && \
|
||||
@@ -14,6 +14,17 @@ docker.start:
|
||||
docker.stop:
|
||||
docker compose --profile all down
|
||||
|
||||
docker.e2e.start:
|
||||
@echo "Starting Redis and cae-resp-proxy for E2E tests..."
|
||||
docker compose --profile e2e up -d --quiet-pull
|
||||
@echo "Waiting for services to be ready..."
|
||||
@sleep 3
|
||||
@echo "Services ready!"
|
||||
|
||||
docker.e2e.stop:
|
||||
@echo "Stopping E2E services..."
|
||||
docker compose --profile e2e down
|
||||
|
||||
test:
|
||||
$(MAKE) docker.start
|
||||
@if [ -z "$(REDIS_VERSION)" ]; then \
|
||||
@@ -38,7 +49,7 @@ test.ci:
|
||||
export RE_CLUSTER=$(RE_CLUSTER) && \
|
||||
export RCE_DOCKER=$(RCE_DOCKER) && \
|
||||
export REDIS_VERSION=$(REDIS_VERSION) && \
|
||||
go mod tidy -compat=1.18 && \
|
||||
go mod tidy && \
|
||||
go vet && \
|
||||
go test -v -coverprofile=coverage.txt -covermode=atomic ./... -race -skip Example); \
|
||||
done
|
||||
@@ -52,7 +63,7 @@ test.ci.skip-vectorsets:
|
||||
export RE_CLUSTER=$(RE_CLUSTER) && \
|
||||
export RCE_DOCKER=$(RCE_DOCKER) && \
|
||||
export REDIS_VERSION=$(REDIS_VERSION) && \
|
||||
go mod tidy -compat=1.18 && \
|
||||
go mod tidy && \
|
||||
go vet && \
|
||||
go test -v -coverprofile=coverage.txt -covermode=atomic ./... -race \
|
||||
-run '^(?!.*(?:VectorSet|vectorset|ExampleClient_vectorset)).*$$' -skip Example); \
|
||||
@@ -66,7 +77,31 @@ bench:
|
||||
export REDIS_VERSION=$(REDIS_VERSION) && \
|
||||
go test ./... -test.run=NONE -test.bench=. -test.benchmem -skip Example
|
||||
|
||||
.PHONY: all test test.ci test.ci.skip-vectorsets bench fmt
|
||||
test.e2e:
|
||||
@echo "Running E2E tests with auto-start proxy..."
|
||||
$(MAKE) docker.e2e.start
|
||||
@echo "Running tests..."
|
||||
@E2E_SCENARIO_TESTS=true go test -v ./maintnotifications/e2e/ -timeout 30m || ($(MAKE) docker.e2e.stop && exit 1)
|
||||
$(MAKE) docker.e2e.stop
|
||||
@echo "E2E tests completed!"
|
||||
|
||||
test.e2e.docker:
|
||||
@echo "Running Docker-compatible E2E tests..."
|
||||
$(MAKE) docker.e2e.start
|
||||
@echo "Running unified injector tests..."
|
||||
@E2E_SCENARIO_TESTS=true go test -v -run "TestUnifiedInjector|TestCreateTestFaultInjectorLogic|TestFaultInjectorClientCreation" ./maintnotifications/e2e/ -timeout 10m || ($(MAKE) docker.e2e.stop && exit 1)
|
||||
$(MAKE) docker.e2e.stop
|
||||
@echo "Docker E2E tests completed!"
|
||||
|
||||
test.e2e.logic:
|
||||
@echo "Running E2E logic tests (no proxy required)..."
|
||||
@E2E_SCENARIO_TESTS=true \
|
||||
REDIS_ENDPOINTS_CONFIG_PATH=/tmp/test_endpoints_verify.json \
|
||||
FAULT_INJECTION_API_URL=http://localhost:8080 \
|
||||
go test -v -run "TestCreateTestFaultInjectorLogic|TestFaultInjectorClientCreation" ./maintnotifications/e2e/
|
||||
@echo "Logic tests completed!"
|
||||
|
||||
.PHONY: all test test.ci test.ci.skip-vectorsets bench fmt test.e2e test.e2e.logic docker.e2e.start docker.e2e.stop
|
||||
|
||||
build:
|
||||
export RE_CLUSTER=$(RE_CLUSTER) && \
|
||||
@@ -83,5 +118,5 @@ go_mod_tidy:
|
||||
echo "go mod tidy in $${dir}"; \
|
||||
(cd "$${dir}" && \
|
||||
go get -u ./... && \
|
||||
go mod tidy -compat=1.18); \
|
||||
go mod tidy); \
|
||||
done
|
||||
|
||||
@@ -18,16 +18,24 @@
|
||||
|
||||
In `go-redis` we are aiming to support the last three releases of Redis. Currently, this means we do support:
|
||||
- [Redis 8.0](https://raw.githubusercontent.com/redis/redis/8.0/00-RELEASENOTES) - using Redis CE 8.0
|
||||
- [Redis 8.2](https://raw.githubusercontent.com/redis/redis/8.2/00-RELEASENOTES) - using Redis CE 8.2
|
||||
- [Redis 8.2](https://raw.githubusercontent.com/redis/redis/8.2/00-RELEASENOTES) - using Redis CE 8.2
|
||||
- [Redis 8.4](https://raw.githubusercontent.com/redis/redis/8.4/00-RELEASENOTES) - using Redis CE 8.4
|
||||
- [Redis 8.8](https://raw.githubusercontent.com/redis/redis/8.8/00-RELEASENOTES) - using Redis CE 8.8
|
||||
|
||||
Although the `go.mod` states it requires at minimum `go 1.21`, our CI is configured to run the tests against all three
|
||||
versions of Redis and multiple versions of Go ([1.21](https://go.dev/doc/devel/release#go1.21.0),
|
||||
[1.23](https://go.dev/doc/devel/release#go1.23.0), oldstable, and stable). We observe that some modules related test may not pass with
|
||||
Although the `go.mod` states it requires at minimum `go 1.24`, our CI is configured to run the tests against all supported
|
||||
versions of Redis and multiple versions of Go ([1.24](https://go.dev/doc/devel/release#go1.24.0), oldstable, and stable). We observe that some modules related test may not pass with
|
||||
Redis Stack 7.2 and some commands are changed with Redis CE 8.0.
|
||||
Although it is not officially supported, `go-redis/v9` should be able to work with any Redis 7.0+.
|
||||
Please do refer to the documentation and the tests if you experience any issues.
|
||||
|
||||
### Array data type (Redis 8.8+)
|
||||
|
||||
Starting with Redis 8.8, go-redis exposes the new array data type via the `AR*` command family
|
||||
(`ARSET`, `ARGET`, `ARGETRANGE`, `ARMSET`, `ARMGET`, `ARINSERT`, `ARDEL`, `ARDELRANGE`,
|
||||
`ARLEN`, `ARCOUNT`, `ARNEXT`, `ARSEEK`, `ARSCAN`, `ARGREP`, `ARRING`, `ARLASTITEMS`,
|
||||
`ARINFO`/`ARINFOFULL`, and the `AROP*` reducers). See `array_commands.go` for the full
|
||||
surface. The API is experimental and may change in a future release.
|
||||
|
||||
## How do I Redis?
|
||||
|
||||
[Learn for free at Redis University](https://university.redis.com/)
|
||||
@@ -49,6 +57,7 @@ Please do refer to the documentation and the tests if you experience any issues.
|
||||
- [Chat](https://discord.gg/W4txy5AeKM)
|
||||
- [Reference](https://pkg.go.dev/github.com/redis/go-redis/v9)
|
||||
- [Examples](https://pkg.go.dev/github.com/redis/go-redis/v9#pkg-examples)
|
||||
- [Release notes](./RELEASE-NOTES.md) ([GitHub Releases](https://github.com/redis/go-redis/releases))
|
||||
|
||||
## old documentation
|
||||
|
||||
@@ -136,6 +145,29 @@ func ExampleClient() {
|
||||
}
|
||||
```
|
||||
|
||||
### Dial retries and backoff
|
||||
|
||||
Connection establishment can be retried by the connection pool when dialing fails.
|
||||
|
||||
- **`DialerRetries`**: maximum number of dial attempts (default: 5).
|
||||
- **`DialerRetryTimeout`**: default delay between attempts when no custom backoff is provided (default: 100ms).
|
||||
- **`DialerRetryBackoff`**: optional function hook to control the delay between attempts.
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6379",
|
||||
|
||||
DialerRetries: 5,
|
||||
DialerRetryTimeout: 100 * time.Millisecond, // used when DialerRetryBackoff is nil
|
||||
|
||||
// Optional: exponential backoff with jitter and a cap.
|
||||
DialerRetryBackoff: redis.DialRetryBackoffExponential(100*time.Millisecond, 2*time.Second),
|
||||
})
|
||||
defer rdb.Close()
|
||||
```
|
||||
|
||||
### Authentication
|
||||
|
||||
The Redis client supports multiple ways to provide authentication credentials, with a clear priority order. Here are the available options:
|
||||
@@ -333,18 +365,17 @@ rdb := redis.NewClient(&redis.Options{
|
||||
})
|
||||
```
|
||||
|
||||
#### Unstable RESP3 Structures for RediSearch Commands
|
||||
When integrating Redis with application functionalities using RESP3, it's important to note that some response structures aren't final yet. This is especially true for more complex structures like search and query results. We recommend using RESP2 when using the search and query capabilities, but we plan to stabilize the RESP3-based API-s in the coming versions. You can find more guidance in the upcoming release notes.
|
||||
#### RESP3 for RediSearch Commands (`UnstableResp3` is deprecated)
|
||||
As of v9.20, `FT.SEARCH`, `FT.AGGREGATE`, `FT.INFO`, `FT.SPELLCHECK`, and `FT.SYNDUMP`
|
||||
parse RESP3 (map) responses into the same typed result objects as RESP2. **No flag
|
||||
is required — `Val()` / `Result()` work uniformly on both protocols.**
|
||||
|
||||
To enable unstable RESP3, set the option in your client configuration:
|
||||
The legacy `UnstableResp3` option is now a **no-op** and is retained on every
|
||||
options struct only for backwards compatibility. It will be removed in a future
|
||||
release; new code should not set it.
|
||||
|
||||
```go
|
||||
redis.NewClient(&redis.Options{
|
||||
UnstableResp3: true,
|
||||
})
|
||||
```
|
||||
**Note:** When UnstableResp3 mode is enabled, it's necessary to use RawResult() and RawVal() to retrieve a raw data.
|
||||
Since, raw response is the only option for unstable search commands Val() and Result() calls wouldn't have any affect on them:
|
||||
`RawResult()` / `RawVal()` continue to work for callers that prefer the raw RESP
|
||||
payload directly:
|
||||
|
||||
```go
|
||||
res1, err := client.FTSearchWithArgs(ctx, "txt", "foo bar", &redis.FTSearchOptions{}).RawResult()
|
||||
|
||||
@@ -1,5 +1,261 @@
|
||||
# Release Notes
|
||||
|
||||
# 9.20.0 (2026-05-28)
|
||||
|
||||
## 🚀 Highlights
|
||||
|
||||
### Redis 8.8 Support
|
||||
|
||||
This release adds support for **Redis 8.8**. The README's supported-versions list now includes Redis 8.8 alongside 8.0/8.2/8.4, and CI exercises the `8.8-rc1` client-libs-test image across the full suite (Makefile, build workflow, doctests, run-tests action, and docker-compose).
|
||||
|
||||
Coverage for the new commands that ship in the 8.x line, rounded out in this release:
|
||||
|
||||
- **`AR*` array data type** ([#3813](https://github.com/redis/go-redis/pull/3813)) — new array data structure, exposed via the `ArrayCmdable` interface (see the experimental-features highlight below).
|
||||
- **`INCREX`** ([#3816](https://github.com/redis/go-redis/pull/3816)) — atomic increment with expiration in a single round-trip.
|
||||
- **`XNACK`** ([#3790](https://github.com/redis/go-redis/pull/3790)) — explicit negative-acknowledge of pending stream entries.
|
||||
- **`XAUTOCLAIM` PEL deletes** ([#3798](https://github.com/redis/go-redis/pull/3798)) — `XAUTOCLAIM`/`XAUTOCLAIMJUSTID` now return the list of deleted message IDs from the pending entries list.
|
||||
- **`TS.RANGE` multiple aggregators** ([#3791](https://github.com/redis/go-redis/pull/3791)) — `TS.RANGE`/`TS.REVRANGE`/`TS.MRANGE`/`TS.MREVRANGE` accept multiple aggregators in a single call.
|
||||
- **`Z(UNION|INTER|DIFF)` `COUNT` aggregator** ([#3802](https://github.com/redis/go-redis/pull/3802)) — `COUNT` reducer for sorted-set set operations.
|
||||
- **`JSON.SET FPHA`** ([#3797](https://github.com/redis/go-redis/pull/3797)) — new `FPHA` argument that specifies the floating-point type for homogeneous FP arrays.
|
||||
|
||||
CI image bump ([#3814](https://github.com/redis/go-redis/pull/3814)) by [@ofekshenawa](https://github.com/ofekshenawa). Command coverage contributions by [@cxljs](https://github.com/cxljs), [@elena-kolevska](https://github.com/elena-kolevska), [@Khukharr](https://github.com/Khukharr), [@ndyakov](https://github.com/ndyakov), and [@ofekshenawa](https://github.com/ofekshenawa).
|
||||
|
||||
### Stable RESP3 for RediSearch (`UnstableResp3` deprecated)
|
||||
|
||||
`FT.SEARCH`, `FT.AGGREGATE`, `FT.INFO`, `FT.SPELLCHECK`, and `FT.SYNDUMP` now parse RESP3 (map) responses into the same typed result objects as RESP2 — `Val()` and `Result()` work uniformly on both protocols, no flag required. Previously, RESP3 search responses required `UnstableResp3: true` and were returned as opaque maps accessible only via `RawResult()` / `RawVal()`.
|
||||
|
||||
As a result, the `UnstableResp3` option is now a **no-op** across every options struct (`Options`, `ClusterOptions`, `UniversalOptions`, `FailoverOptions`, `RingOptions`) and has been marked `// Deprecated:`. The field is retained for backwards compatibility — existing code that sets `UnstableResp3: true` will continue to compile and behave identically — but it will be removed in a future release and new code should not set it. `RawResult()` / `RawVal()` continue to work for callers that prefer the raw RESP payload.
|
||||
|
||||
([#3741](https://github.com/redis/go-redis/pull/3741)) by [@ndyakov](https://github.com/ndyakov)
|
||||
|
||||
### Experimental Array Data Structure Commands
|
||||
|
||||
Adds an experimental `ArrayCmdable` interface with the `AR*` command family (`ARSet`, `ARGet`, `ARGetRange`, `ARMSet`, `ARMGet`, `ARDel`, `ARDelRange`, `ARScan`, `ARSeek`, `ARNext`, `ARLastItems`, `ARGrep`, `ARGrepWithValues`, `ARInfo`/`ARInfoFull`, and typed reducers `AROpSum`/`AROpMin`/`AROpMax`/`AROpAnd`/`AROpOr`/`AROpXor`/`AROpMatch`/`AROpUsed`) for working with Redis 8.8's new array data type. **API is experimental and may change in a future release.**
|
||||
|
||||
([#3813](https://github.com/redis/go-redis/pull/3813)) by [@cxljs](https://github.com/cxljs)
|
||||
|
||||
## ✨ New Features
|
||||
|
||||
- **RESP3 search parser**: First-class RESP3 parsing for `FT.SEARCH`/`FT.AGGREGATE`/`FT.INFO`/`FT.SPELLCHECK`/`FT.SYNDUMP` responses with backwards compatibility for RESP2 ([#3741](https://github.com/redis/go-redis/pull/3741)) by [@ndyakov](https://github.com/ndyakov)
|
||||
- **INCREX**: New `INCREX` command support — atomic increment with expiration ([#3816](https://github.com/redis/go-redis/pull/3816)) by [@ndyakov](https://github.com/ndyakov)
|
||||
- **XNACK**: Client support for the `XNACK` stream command for explicitly negative-acknowledging pending entries ([#3790](https://github.com/redis/go-redis/pull/3790)) by [@elena-kolevska](https://github.com/elena-kolevska)
|
||||
- **TS range multiple aggregators**: `TS.RANGE`/`TS.REVRANGE`/`TS.MRANGE`/`TS.MREVRANGE` now accept multiple aggregators in a single call ([#3791](https://github.com/redis/go-redis/pull/3791)) by [@elena-kolevska](https://github.com/elena-kolevska)
|
||||
- **`XAutoClaim` deleted IDs**: `XAUTOCLAIM`/`XAUTOCLAIMJUSTID` now return the list of deleted message IDs from the PEL ([#3798](https://github.com/redis/go-redis/pull/3798)) by [@Khukharr](https://github.com/Khukharr)
|
||||
- **`JSON.SET FPHA`**: `JSON.SET` accepts a new `FPHA` argument that specifies the floating-point type for homogeneous floating-point arrays ([#3797](https://github.com/redis/go-redis/pull/3797)) by [@ndyakov](https://github.com/ndyakov)
|
||||
- **Sorted-set union/intersection COUNT**: `ZUNION`/`ZINTER`/`ZDIFF` aggregator now supports `COUNT` ([#3802](https://github.com/redis/go-redis/pull/3802)) by [@ofekshenawa](https://github.com/ofekshenawa)
|
||||
- **`FT.HYBRID` vector validation**: Validates hybrid-search vector input types and adds proper typed vector parameters ([#3756](https://github.com/redis/go-redis/pull/3756)) by [@DengY11](https://github.com/DengY11)
|
||||
- **Cluster pool wait stats**: `ClusterClient.PoolStats()` now accumulates `WaitCount` and `WaitDurationNs` across all node pools (previously always zero) ([#3809](https://github.com/redis/go-redis/pull/3809)) by [@LINKIWI](https://github.com/LINKIWI)
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
- **TLS-only Cluster PubSub**: `CLUSTER SLOTS` port-0 entries now fall back to the origin endpoint's port, fixing `dial tcp <ip>:0: connection refused` on TLS-only clusters started with `--port 0 --tls-port <port>` (fixes [#3726](https://github.com/redis/go-redis/issues/3726)) ([#3828](https://github.com/redis/go-redis/pull/3828)) by [@ndyakov](https://github.com/ndyakov)
|
||||
- **Sharded PubSub reconnect routing**: `PubSub.conn()` now passes both regular (`c.channels`) and sharded (`c.schannels`) channels into the per-PubSub `newConn` closure. Previously, `ClusterClient.SSubscribe`-only PubSubs reconnected to a random node (because the routing closure saw an empty channel list), the `SSUBSCRIBE` was sent to the wrong shard, and the resulting `MOVED` reply was silently dropped ([#3829](https://github.com/redis/go-redis/pull/3829)) by [@ndyakov](https://github.com/ndyakov)
|
||||
- **ClusterClient `Watch` retry**: User errors returned from a `Watch` callback are no longer subjected to cluster-retry classification; transient cluster errors still retry, but a callback returning e.g. `net.ErrClosed` short-circuits immediately ([#3821](https://github.com/redis/go-redis/pull/3821)) by [@obiyang](https://github.com/obiyang)
|
||||
- **Sentinel concurrent-probe leak**: `MasterAddr`'s concurrent sentinel probe now closes the non-winning sentinel clients instead of leaking them ([#3827](https://github.com/redis/go-redis/pull/3827)) by [@cxljs](https://github.com/cxljs)
|
||||
- **Sentinel rediscovery loop on master-only setups**: `replicaAddrs` no longer tears down the cached sentinel client when the replica list is empty, eliminating a continuous rediscovery loop on master-only Sentinel deployments that flooded logs and added per-operation latency ([#3795](https://github.com/redis/go-redis/pull/3795)) by [@shahyash2609](https://github.com/shahyash2609)
|
||||
- **Pool `CloseConn` hooks**: `Pool.CloseConn` now triggers registered hooks, fixing a memory leak when connections are closed explicitly rather than via the normal removal path ([#3818](https://github.com/redis/go-redis/pull/3818)) by [@ndyakov](https://github.com/ndyakov)
|
||||
- **Dial TCP error redirection**: Wrapped `dial tcp` errors are now correctly classified as redirectable so cluster routing can recover from a single unreachable node ([#3810](https://github.com/redis/go-redis/pull/3810)) by [@vladisa88](https://github.com/vladisa88)
|
||||
- **Pool `Close` health checks**: `ConnPool.Close` now only runs health checks against idle connections, avoiding spurious activity on connections still in use ([#3805](https://github.com/redis/go-redis/pull/3805)) by [@ndyakov](https://github.com/ndyakov)
|
||||
- **VLinks return type**: Fixed the return type of `VLINKS`/`VLINKSWITHSCORES` vector-set replies ([#3820](https://github.com/redis/go-redis/pull/3820)) by [@romanpovol](https://github.com/romanpovol)
|
||||
|
||||
## 🧪 Testing & Infrastructure
|
||||
|
||||
- **Flaky tests**: Stabilized several flaky tests in the sentinel and pool suites ([#3815](https://github.com/redis/go-redis/pull/3815)) by [@ndyakov](https://github.com/ndyakov)
|
||||
- **Sentinel failover metric race**: Fixed a data race in the sentinel failover metric test ([#3824](https://github.com/redis/go-redis/pull/3824)) by [@cxljs](https://github.com/cxljs)
|
||||
- **`waitForSentinelClusterStable` post-conditions**: The sentinel test harness now waits for replicas to be fully connected (not just present in the count) and is robust to randomized spec ordering after failover specs, eliminating an intermittent `Expected master to equal slave` flake ([#3830](https://github.com/redis/go-redis/pull/3830)) by [@ndyakov](https://github.com/ndyakov)
|
||||
- **`govulncheck` workflow**: New scheduled GitHub Actions workflow runs `govulncheck` on every push, PR, and weekly, surfacing newly disclosed Go vulnerabilities even when no code changes ([#3779](https://github.com/redis/go-redis/pull/3779)) by [@solardome](https://github.com/solardome)
|
||||
- **CI Redis 8.8-rc1**: CI now exercises the 8.8-rc1 Redis image ([#3814](https://github.com/redis/go-redis/pull/3814)) by [@ofekshenawa](https://github.com/ofekshenawa)
|
||||
|
||||
## 🧰 Maintenance
|
||||
|
||||
- **`Cmd.Slot()` lookup refactor**: Caches the per-command `CommandInfo` and short-circuits keyless commands before the switch dispatch, removing redundant `Peek` calls ([#3804](https://github.com/redis/go-redis/pull/3804)) by [@retr0-kernel](https://github.com/retr0-kernel)
|
||||
- **stdlib `math/rand`**: Replaced `internal/rand` with `math/rand` from the standard library now that the minimum Go version is 1.24 ([#3823](https://github.com/redis/go-redis/pull/3823)) by [@cxljs](https://github.com/cxljs)
|
||||
- **ConnPool queue channel**: Removed the unused queue channel from `ConnPool`, trimming the pool's footprint ([#3826](https://github.com/redis/go-redis/pull/3826)) by [@cxljs](https://github.com/cxljs)
|
||||
- **Extra packages LICENSE**: Added a LICENSE file to each `extra/*` package ([#3817](https://github.com/redis/go-redis/pull/3817)) by [@ndyakov](https://github.com/ndyakov)
|
||||
- **README & CI image**: Documentation refresh and bumped the default CI image tag ([#3822](https://github.com/redis/go-redis/pull/3822)) by [@ndyakov](https://github.com/ndyakov)
|
||||
|
||||
## 👥 Contributors
|
||||
|
||||
We'd like to thank all the contributors who worked on this release!
|
||||
|
||||
[@cxljs](https://github.com/cxljs), [@DengY11](https://github.com/DengY11), [@elena-kolevska](https://github.com/elena-kolevska), [@Khukharr](https://github.com/Khukharr), [@LINKIWI](https://github.com/LINKIWI), [@ndyakov](https://github.com/ndyakov), [@obiyang](https://github.com/obiyang), [@ofekshenawa](https://github.com/ofekshenawa), [@retr0-kernel](https://github.com/retr0-kernel), [@romanpovol](https://github.com/romanpovol), [@shahyash2609](https://github.com/shahyash2609), [@solardome](https://github.com/solardome), [@vladisa88](https://github.com/vladisa88)
|
||||
|
||||
---
|
||||
|
||||
**Full Changelog**: https://github.com/redis/go-redis/compare/v9.19.0...v9.20.0
|
||||
|
||||
# 9.19.0 (2026-04-27)
|
||||
|
||||
## 🚀 Highlights
|
||||
|
||||
### FIPS-Compatible Script Helper
|
||||
|
||||
`Script` now supports a FIPS-safe execution mode that avoids client-side SHA-1 computation, which is blocked in strict FIPS environments. A new `NewScriptServerSHA` constructor uses `SCRIPT LOAD` to obtain and cache the digest from the server, then runs commands via `EVALSHA`/`EVALSHA_RO`. Falls back to `EVAL`/`EVALRO` if loading fails, and transparently retries once on `NOSCRIPT`. The default behavior is unchanged for existing users.
|
||||
|
||||
([#3700](https://github.com/redis/go-redis/pull/3700)) by [@chaitanyabodlapati](https://github.com/chaitanyabodlapati)
|
||||
|
||||
### FT.AGGREGATE Step-Based Pipeline Builder
|
||||
|
||||
Added a new step-based `FT.AGGREGATE` pipeline API via `FTAggregateOptions.Steps`, allowing `LOAD`, `APPLY`, `GROUPBY`, and `SORTBY` (with per-step `MAX`) to be repeated and interleaved in arbitrary order — matching Redis's native multi-stage aggregation semantics. The legacy `Load`/`Apply`/`GroupBy`/`SortBy`/`SortByMax` fields are now deprecated.
|
||||
|
||||
([#3782](https://github.com/redis/go-redis/pull/3782)) by [@ndyakov](https://github.com/ndyakov)
|
||||
|
||||
### Raw RESP Protocol Access
|
||||
|
||||
Added `DoRaw` and `DoRawWriteTo` methods for executing arbitrary commands and reading the raw RESP response. Useful for proxying, custom protocol inspection, and working with commands not yet wrapped by go-redis.
|
||||
|
||||
([#3713](https://github.com/redis/go-redis/pull/3713)) by [@ofekshenawa](https://github.com/ofekshenawa)
|
||||
|
||||
### Configurable Dial Retry Backoff
|
||||
|
||||
Added `DialerRetryBackoff` option (plumbed through `Options`, `ClusterOptions`, `RingOptions`, `FailoverOptions`) to let callers customize the delay between failed dial attempts. Helpers `DialRetryBackoffConstant` and `DialRetryBackoffExponential` (with jitter and cap) are provided out of the box. Dial timeout is now also applied **per attempt** rather than across all retries.
|
||||
|
||||
([#3706](https://github.com/redis/go-redis/pull/3706), [#3705](https://github.com/redis/go-redis/pull/3705)) by [@mwhooker](https://github.com/mwhooker)
|
||||
|
||||
## ✨ New Features
|
||||
|
||||
- **FT.AGGREGATE Steps**: Step-based pipeline builder for `FT.AGGREGATE` with support for repeated/interleaved `LOAD`, `APPLY`, `GROUPBY`, and `SORTBY` stages ([#3782](https://github.com/redis/go-redis/pull/3782)) by [@ndyakov](https://github.com/ndyakov)
|
||||
- **VectorSet commands**: Added `VISMEMBER` and `WITHATTRIBS` support ([#3753](https://github.com/redis/go-redis/pull/3753)) by [@romanpovol](https://github.com/romanpovol)
|
||||
- **FIPS-safe Script**: `NewScriptServerSHA` uses `SCRIPT LOAD` to obtain the digest from the server, avoiding client-side SHA-1 ([#3700](https://github.com/redis/go-redis/pull/3700)) by [@chaitanyabodlapati](https://github.com/chaitanyabodlapati)
|
||||
- **Raw RESP access**: `DoRaw` and `DoRawWriteTo` for raw RESP protocol access ([#3713](https://github.com/redis/go-redis/pull/3713)) by [@ofekshenawa](https://github.com/ofekshenawa)
|
||||
- **Dial retry backoff**: `DialerRetryBackoff` function option with constant and exponential helpers ([#3706](https://github.com/redis/go-redis/pull/3706)) by [@mwhooker](https://github.com/mwhooker)
|
||||
- **Typed NOSCRIPT error**: Redis `NOSCRIPT` replies are now surfaced as a typed error for easier handling ([#3738](https://github.com/redis/go-redis/pull/3738)) by [@LINKIWI](https://github.com/LINKIWI)
|
||||
- **PubSub ClientSetName**: Added `ClientSetName` method to `PubSub` ([#3727](https://github.com/redis/go-redis/pull/3727)) by [@Flack74](https://github.com/Flack74)
|
||||
- **ReplicaOf**: New `ReplicaOf` method replaces the deprecated `SlaveOf` ([#3720](https://github.com/redis/go-redis/pull/3720)) by [@Copilot](https://github.com/apps/copilot-swe-agent)
|
||||
- **HSCAN BinaryUnmarshaler**: `HScan` now supports types implementing `encoding.BinaryUnmarshaler` ([#3768](https://github.com/redis/go-redis/pull/3768)) by [@Aaditya-dubey1](https://github.com/Aaditya-dubey1)
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
- **Auto hostname type detection**: Improved endpoint type detection for maintenance notifications using DNS-based classification; handles empty hosts and expanded private-IP ranges ([#3789](https://github.com/redis/go-redis/pull/3789)) by [@ndyakov](https://github.com/ndyakov)
|
||||
- **HELLO fallback**: Don't send `CLIENT MAINT_NOTIFICATIONS` handshake when `HELLO` fails and connection falls back to RESP2; fail fast when explicitly enabled with RESP3 ([#3788](https://github.com/redis/go-redis/pull/3788)) by [@ndyakov](https://github.com/ndyakov)
|
||||
- **Dial TCP retry**: `ShouldRetry` now treats `net.OpError` with `Op == "dial"` timeout errors as safe to retry since no command was sent ([#3787](https://github.com/redis/go-redis/pull/3787)) by [@vladisa88](https://github.com/vladisa88)
|
||||
- **wrappedOnClose leak**: Fixed resource leak caused by repeatedly wrapping `baseClient` close logic; replaced with a bounded, concurrency-safe named-hook registry ([#3785](https://github.com/redis/go-redis/pull/3785)) by [@ndyakov](https://github.com/ndyakov)
|
||||
- **Pool Close() on stale connections**: Suppress close errors (e.g., TLS `closeNotify` timeouts) for connections already dropped by the server due to idle timeout ([#3778](https://github.com/redis/go-redis/pull/3778)) by [@ofekshenawa](https://github.com/ofekshenawa)
|
||||
- **FIFO waiter ordering**: Fixed race in `ConnStateMachine.notifyWaiters` that could wake multiple waiters under a single mutex hold and violate FIFO ordering ([#3777](https://github.com/redis/go-redis/pull/3777)) by [@0x48core](https://github.com/0x48core)
|
||||
- **Lua READONLY detection**: Detect `READONLY` errors embedded in Lua script error messages on read-only replicas so commands are correctly retried ([#3769](https://github.com/redis/go-redis/pull/3769)) by [@zhengjilei](https://github.com/zhengjilei)
|
||||
- **VectorScoreSliceCmd RESP2**: Fixed `VSimWithScores`, `VSimWithArgsWithScores`, and `VLinksWithScores` which were broken on RESP2 connections returning flat arrays instead of maps ([#3767](https://github.com/redis/go-redis/pull/3767)) by [@Copilot](https://github.com/apps/copilot-swe-agent)
|
||||
- **Closed connection handling**: Two fixes for closed connection handling in the pool ([#3764](https://github.com/redis/go-redis/pull/3764)) by [@cxljs](https://github.com/cxljs)
|
||||
- **ZRangeArgs Rev**: Fixed `ZRangeArgs` with `Rev` + `ByScore`/`ByLex` incorrectly swapping `Start`/`Stop`, breaking `ZRANGESTORE` ([#3751](https://github.com/redis/go-redis/pull/3751)) by [@Copilot](https://github.com/apps/copilot-swe-agent)
|
||||
- **OTel metric instrument types**: Fixed metric instrument types in `redisotel-native` ([#3743](https://github.com/redis/go-redis/pull/3743)) by [@ofekshenawa](https://github.com/ofekshenawa)
|
||||
- **Options.clone() data race**: Fixed data race when cloning `Options` ([#3739](https://github.com/redis/go-redis/pull/3739)) by [@rubensayshi](https://github.com/rubensayshi)
|
||||
- **Connection closure metrics**: Fixed connection closure metrics and enabled all metric groups by default in `redisotel-native` ([#3735](https://github.com/redis/go-redis/pull/3735)) by [@ofekshenawa](https://github.com/ofekshenawa)
|
||||
- **OTel semconv v1.38.0**: Use metric definition from `otel/semconv/v1.38.0` in `redisotel-native` ([#3731](https://github.com/redis/go-redis/pull/3731)) by [@wzy9607](https://github.com/wzy9607)
|
||||
- **SETNX semantics**: Use `SET ... NX` instead of the deprecated `SETNX` command ([#3723](https://github.com/redis/go-redis/pull/3723)) by [@ndyakov](https://github.com/ndyakov)
|
||||
- **TIME keyless routing**: Mark `TIME` as a keyless command for correct cluster routing ([#3722](https://github.com/redis/go-redis/pull/3722)) by [@fatal10110](https://github.com/fatal10110)
|
||||
- **Dial timeout per retry**: Dial timeout now applies per attempt instead of across all retry attempts combined ([#3705](https://github.com/redis/go-redis/pull/3705)) by [@mwhooker](https://github.com/mwhooker)
|
||||
- **Cluster metrics attributes**: Fixed `pool.name` being appended per node, which corrupted and dropped user-provided custom attributes ([#3699](https://github.com/redis/go-redis/pull/3699)) by [@Jesse-Bonfire](https://github.com/Jesse-Bonfire)
|
||||
- **initConn nil dereference**: Fixed nil pointer dereference and potential deadlock in `*baseClient.initConn()`; added explicit nil option guards to client constructors ([#3676](https://github.com/redis/go-redis/pull/3676)) by [@olde-ducke](https://github.com/olde-ducke)
|
||||
|
||||
## ⚡ Performance
|
||||
|
||||
- **RESP reader**: Optimized RESP reader by eliminating intermediate string allocations ([#3774](https://github.com/redis/go-redis/pull/3774)) by [@Aaditya-dubey1](https://github.com/Aaditya-dubey1)
|
||||
- **Inline rendezvous hashing**: Replaced `github.com/dgryski/go-rendezvous` dependency with an in-repo implementation in `internal/hashtag`, reducing the dependency graph while preserving algorithm parity ([#3762](https://github.com/redis/go-redis/pull/3762)) by [@bigsk05](https://github.com/bigsk05)
|
||||
|
||||
## 🧪 Testing & Infrastructure
|
||||
|
||||
- **Release automation**: Added `repository`, `ref`, and `client-libs-test-image-tag` inputs to the `run-tests` composite action; `redis-version` is now optional so unstable builds use `REDIS_VERSION` from the Makefile ([#3749](https://github.com/redis/go-redis/pull/3749)) by [@dariaguy](https://github.com/dariaguy)
|
||||
- **Go 1.24**: Updated minimum Go version to 1.24 and use `-compat=1.24` in release scripts ([#3714](https://github.com/redis/go-redis/pull/3714), [#3754](https://github.com/redis/go-redis/pull/3754)) by [@ndyakov](https://github.com/ndyakov), [@cxljs](https://github.com/cxljs)
|
||||
|
||||
## 🧰 Maintenance
|
||||
|
||||
- **Pool state machine**: Removed redundant `Conn.closed` atomic field in favor of the state machine's `StateClosed` ([#3783](https://github.com/redis/go-redis/pull/3783)) by [@cxljs](https://github.com/cxljs)
|
||||
- **OTel SDK**: Updated OpenTelemetry SDK dependencies in `redisotel`/`redisotel-native` ([#3770](https://github.com/redis/go-redis/pull/3770)) by [@ndyakov](https://github.com/ndyakov)
|
||||
- **Go 1.21+ built-ins**: Use `maps.Keys`, `slices.Collect`, `slices.Contains`, `clear()`, and `slices.SortFunc` instead of custom helpers ([#3758](https://github.com/redis/go-redis/pull/3758), [#3746](https://github.com/redis/go-redis/pull/3746)) by [@cxljs](https://github.com/cxljs)
|
||||
- **HGetAll docs**: Added Go doc comment to `HGetAll` describing behavior and complexity ([#3776](https://github.com/redis/go-redis/pull/3776)) by [@0x48core](https://github.com/0x48core)
|
||||
- **Docs links**: Fixed irrelevant docs links ([#3724](https://github.com/redis/go-redis/pull/3724)) by [@olzhas-sabiyev](https://github.com/olzhas-sabiyev)
|
||||
- **Examples cleanup**: Removed throughput binary from examples ([#3733](https://github.com/redis/go-redis/pull/3733)) by [@ndyakov](https://github.com/ndyakov)
|
||||
|
||||
## 👥 Contributors
|
||||
|
||||
We'd like to thank all the contributors who worked on this release!
|
||||
|
||||
[@0x48core](https://github.com/0x48core), [@Aaditya-dubey1](https://github.com/Aaditya-dubey1), [@Copilot](https://github.com/apps/copilot-swe-agent), [@Flack74](https://github.com/Flack74), [@Jesse-Bonfire](https://github.com/Jesse-Bonfire), [@LINKIWI](https://github.com/LINKIWI), [@bigsk05](https://github.com/bigsk05), [@chaitanyabodlapati](https://github.com/chaitanyabodlapati), [@cxljs](https://github.com/cxljs), [@dariaguy](https://github.com/dariaguy), [@fatal10110](https://github.com/fatal10110), [@mwhooker](https://github.com/mwhooker), [@ndyakov](https://github.com/ndyakov), [@ofekshenawa](https://github.com/ofekshenawa), [@olde-ducke](https://github.com/olde-ducke), [@olzhas-sabiyev](https://github.com/olzhas-sabiyev), [@romanpovol](https://github.com/romanpovol), [@rubensayshi](https://github.com/rubensayshi), [@vladisa88](https://github.com/vladisa88), [@wzy9607](https://github.com/wzy9607), [@zhengjilei](https://github.com/zhengjilei)
|
||||
|
||||
---
|
||||
|
||||
**Full Changelog**: https://github.com/redis/go-redis/compare/v9.18.0...v9.19.0
|
||||
|
||||
# 9.18.0 (2026-02-16)
|
||||
|
||||
## 🚀 Highlights
|
||||
|
||||
### Redis 8.6 Support
|
||||
|
||||
Added support for Redis 8.6, including new commands and features for streams idempotent production and HOTKEYS.
|
||||
|
||||
### Smart Client Handoff (Maintenance Notifications) for Cluster
|
||||
|
||||
This release introduces comprehensive support for Redis Cluster maintenance notifications via SMIGRATING/SMIGRATED push notifications. The client now automatically handles slot migrations by:
|
||||
- **Relaxing timeouts during migration** (SMIGRATING) to prevent false failures
|
||||
- **Triggering lazy cluster state reloads** upon completion (SMIGRATED)
|
||||
- Enabling seamless operations during Redis Enterprise maintenance windows
|
||||
|
||||
([#3643](https://github.com/redis/go-redis/pull/3643)) by [@ndyakov](https://github.com/ndyakov)
|
||||
|
||||
### OpenTelemetry Native Metrics Support
|
||||
|
||||
Added comprehensive OpenTelemetry metrics support following the [OpenTelemetry Database Client Semantic Conventions](https://opentelemetry.io/docs/specs/semconv/database/database-metrics/). The implementation uses a Bridge Pattern to keep the core library dependency-free while providing optional metrics instrumentation through the new `extra/redisotel-native` package.
|
||||
|
||||
**Metric groups include:**
|
||||
- Command metrics: Operation duration with retry tracking
|
||||
- Connection basic: Connection count and creation time
|
||||
- Resiliency: Errors, handoffs, timeout relaxation
|
||||
- Connection advanced: Wait time and use time
|
||||
- Pubsub metrics: Published and received messages
|
||||
- Stream metrics: Processing duration and maintenance notifications
|
||||
|
||||
([#3637](https://github.com/redis/go-redis/pull/3637)) by [@ofekshenawa](https://github.com/ofekshenawa)
|
||||
|
||||
## ✨ New Features
|
||||
|
||||
- **HOTKEYS Commands**: Added support for Redis HOTKEYS feature for identifying hot keys based on CPU consumption and network utilization ([#3695](https://github.com/redis/go-redis/pull/3695)) by [@ofekshenawa](https://github.com/ofekshenawa)
|
||||
- **Streams Idempotent Production**: Added support for Redis 8.6+ Streams Idempotent Production with `ProducerID`, `IdempotentID`, `IdempotentAuto` in `XAddArgs` and new `XCFGSET` command ([#3693](https://github.com/redis/go-redis/pull/3693)) by [@ofekshenawa](https://github.com/ofekshenawa)
|
||||
- **NaN Values for TimeSeries**: Added support for NaN (Not a Number) values in Redis time series commands ([#3687](https://github.com/redis/go-redis/pull/3687)) by [@ofekshenawa](https://github.com/ofekshenawa)
|
||||
- **DialerRetries Options**: Added `DialerRetries` and `DialerRetryTimeout` to `ClusterOptions`, `RingOptions`, and `FailoverOptions` ([#3686](https://github.com/redis/go-redis/pull/3686)) by [@naveenchander30](https://github.com/naveenchander30)
|
||||
- **ConnMaxLifetimeJitter**: Added jitter configuration to distribute connection expiration times and prevent thundering herd ([#3666](https://github.com/redis/go-redis/pull/3666)) by [@cyningsun](https://github.com/cyningsun)
|
||||
- **Digest Helper Functions**: Added `DigestString` and `DigestBytes` helper functions for client-side xxh3 hashing compatible with Redis DIGEST command ([#3679](https://github.com/redis/go-redis/pull/3679)) by [@ofekshenawa](https://github.com/ofekshenawa)
|
||||
- **SMIGRATED New Format**: Updated SMIGRATED parser to support new format and remember original host:port ([#3697](https://github.com/redis/go-redis/pull/3697)) by [@ndyakov](https://github.com/ndyakov)
|
||||
- **Cluster State Reload Interval**: Added cluster state reload interval option for maintenance notifications ([#3663](https://github.com/redis/go-redis/pull/3663)) by [@ndyakov](https://github.com/ndyakov)
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
- **PubSub nil pointer dereference**: Fixed nil pointer dereference in PubSub after `WithTimeout()` - `pubSubPool` is now properly cloned ([#3710](https://github.com/redis/go-redis/pull/3710)) by [@Copilot](https://github.com/apps/copilot-swe-agent)
|
||||
- **MaintNotificationsConfig nil check**: Guard against nil `MaintNotificationsConfig` in `initConn` ([#3707](https://github.com/redis/go-redis/pull/3707)) by [@veeceey](https://github.com/veeceey)
|
||||
- **wantConnQueue zombie elements**: Fixed zombie `wantConn` elements accumulation in `wantConnQueue` ([#3680](https://github.com/redis/go-redis/pull/3680)) by [@cyningsun](https://github.com/cyningsun)
|
||||
- **XADD/XTRIM approx flag**: Fixed XADD and XTRIM to use `=` when approx is false ([#3684](https://github.com/redis/go-redis/pull/3684)) by [@ndyakov](https://github.com/ndyakov)
|
||||
- **Sentinel timeout retry**: When connection to a sentinel times out, attempt to connect to other sentinels ([#3654](https://github.com/redis/go-redis/pull/3654)) by [@cxljs](https://github.com/cxljs)
|
||||
|
||||
## ⚡ Performance
|
||||
|
||||
- **Fuzz test optimization**: Eliminated repeated string conversions, used functional approach for cleaner operation selection ([#3692](https://github.com/redis/go-redis/pull/3692)) by [@feiguoL](https://github.com/feiguoL)
|
||||
- **Pre-allocate capacity**: Pre-allocate slice capacity to prevent multiple capacity expansions ([#3689](https://github.com/redis/go-redis/pull/3689)) by [@feelshu](https://github.com/feelshu)
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
- **Comprehensive TLS tests**: Added comprehensive TLS tests and example for standalone, cluster, and certificate authentication ([#3681](https://github.com/redis/go-redis/pull/3681)) by [@ndyakov](https://github.com/ndyakov)
|
||||
- **Redis 8.6**: Updated CI to use Redis 8.6-pre ([#3685](https://github.com/redis/go-redis/pull/3685)) by [@ndyakov](https://github.com/ndyakov)
|
||||
|
||||
## 🧰 Maintenance
|
||||
|
||||
- **Deprecation warnings**: Added deprecation warnings for commands based on Redis documentation ([#3673](https://github.com/redis/go-redis/pull/3673)) by [@ndyakov](https://github.com/ndyakov)
|
||||
- **Use errors.Join()**: Replaced custom error join function with standard library `errors.Join()` ([#3653](https://github.com/redis/go-redis/pull/3653)) by [@cxljs](https://github.com/cxljs)
|
||||
- **Use Go 1.21 min/max**: Use Go 1.21's built-in min/max functions ([#3656](https://github.com/redis/go-redis/pull/3656)) by [@cxljs](https://github.com/cxljs)
|
||||
- **Proper formatting**: Code formatting improvements ([#3670](https://github.com/redis/go-redis/pull/3670)) by [@12ya](https://github.com/12ya)
|
||||
- **Set commands documentation**: Added comprehensive documentation to all set command methods ([#3642](https://github.com/redis/go-redis/pull/3642)) by [@iamamirsalehi](https://github.com/iamamirsalehi)
|
||||
- **MaxActiveConns docs**: Added default value documentation for `MaxActiveConns` ([#3674](https://github.com/redis/go-redis/pull/3674)) by [@codykaup](https://github.com/codykaup)
|
||||
- **README example update**: Updated README example ([#3657](https://github.com/redis/go-redis/pull/3657)) by [@cxljs](https://github.com/cxljs)
|
||||
- **Cluster maintnotif example**: Added example application for cluster maintenance notifications ([#3651](https://github.com/redis/go-redis/pull/3651)) by [@ndyakov](https://github.com/ndyakov)
|
||||
|
||||
## 👥 Contributors
|
||||
|
||||
We'd like to thank all the contributors who worked on this release!
|
||||
|
||||
[@12ya](https://github.com/12ya), [@Copilot](https://github.com/apps/copilot-swe-agent), [@codykaup](https://github.com/codykaup), [@cxljs](https://github.com/cxljs), [@cyningsun](https://github.com/cyningsun), [@feelshu](https://github.com/feelshu), [@feiguoL](https://github.com/feiguoL), [@iamamirsalehi](https://github.com/iamamirsalehi), [@naveenchander30](https://github.com/naveenchander30), [@ndyakov](https://github.com/ndyakov), [@ofekshenawa](https://github.com/ofekshenawa), [@veeceey](https://github.com/veeceey)
|
||||
|
||||
---
|
||||
|
||||
**Full Changelog**: https://github.com/redis/go-redis/compare/v9.17.0...v9.18.0
|
||||
|
||||
# 9.18.0-beta.2 (2025-12-09)
|
||||
|
||||
## 🚀 Highlights
|
||||
|
||||
+137
-6
@@ -1,15 +1,146 @@
|
||||
# Releasing
|
||||
|
||||
1. Run `release.sh` script which updates versions in go.mod files and pushes a new branch to GitHub:
|
||||
This document is the runbook for cutting a go-redis release. It is intended
|
||||
for maintainers with write/tag access to the repository.
|
||||
|
||||
For the format and style of the release notes themselves, see
|
||||
[.github/RELEASE_NOTES_TEMPLATE.md](./.github/RELEASE_NOTES_TEMPLATE.md).
|
||||
|
||||
## Versioning
|
||||
|
||||
go-redis follows [Semantic Versioning](https://semver.org/):
|
||||
|
||||
- **Patch** (`vX.Y.Z+1`) — bug fixes, no API changes.
|
||||
- **Minor** (`vX.Y+1.0`) — backwards-compatible new features, deprecations.
|
||||
- **Major** (`vX+1.0.0`) — breaking changes. Coordinate with the team first.
|
||||
|
||||
Pre-releases use `vX.Y.Z-beta.N` / `vX.Y.Z-rc.N`.
|
||||
|
||||
## Pre-release checklist
|
||||
|
||||
- [ ] Target branch is `master` and CI is green on the latest commit.
|
||||
- [ ] All PRs intended for this release are merged.
|
||||
- [ ] There are no open issues in the release milestone (if used).
|
||||
- [ ] `CHANGELOG` / release notes have been considered; dependabot-only
|
||||
and doc-only changes are excluded per the template.
|
||||
- [ ] Confirm the next version number and decide if it's a patch / minor / major.
|
||||
|
||||
## 1. Draft the release notes
|
||||
|
||||
1. Open the draft release auto-generated by
|
||||
[release-drafter](.github/release-drafter-config.yml) on GitHub.
|
||||
2. Prepend a new section to [`RELEASE-NOTES.md`](./RELEASE-NOTES.md) using
|
||||
[`.github/RELEASE_NOTES_TEMPLATE.md`](./.github/RELEASE_NOTES_TEMPLATE.md)
|
||||
as the format. Keep the file in chronological order (newest first).
|
||||
3. Pick 3–5 **Highlights** — the most user-facing, impactful changes.
|
||||
4. Remove dependabot bumps and doc-only typo fixes from the lists.
|
||||
5. Verify every PR has a contributor attribution and link.
|
||||
6. Open a PR with just the release-notes change if you want review before
|
||||
bumping versions, otherwise include it in the release PR below.
|
||||
|
||||
## 2. Bump versions and open the release PR
|
||||
|
||||
Create a release branch from `master`:
|
||||
|
||||
```shell
|
||||
TAG=v1.0.0 ./scripts/release.sh
|
||||
git checkout master && git pull --ff-only
|
||||
git checkout -b release/vX.Y.Z
|
||||
```
|
||||
|
||||
2. Open a pull request and wait for the build to finish.
|
||||
|
||||
3. Merge the pull request and run `tag.sh` to create tags for packages:
|
||||
Run the release script on that branch:
|
||||
|
||||
```shell
|
||||
TAG=v1.0.0 ./scripts/tag.sh
|
||||
TAG=vX.Y.Z ./scripts/release.sh
|
||||
```
|
||||
|
||||
What the script does (and explicitly does **not** do):
|
||||
|
||||
- ✅ Validates `TAG` matches the semver regex and isn't already a git tag.
|
||||
- ✅ Rewrites every `redis/go-redis*` line in every sub-module `go.mod` to
|
||||
point at the new `TAG`. Trailing `// indirect` markers are preserved.
|
||||
- ✅ Runs `go mod tidy -compat=1.24` in each sub-module.
|
||||
- ✅ Updates the return value in [`version.go`](./version.go).
|
||||
- ❌ Does **not** switch branches (runs in your current branch).
|
||||
- ❌ Does **not** require a clean working tree (so you can mix it with
|
||||
release-notes edits in the same branch).
|
||||
- ❌ Does **not** commit, tag, or push anything.
|
||||
|
||||
Review and commit the changes yourself:
|
||||
|
||||
```shell
|
||||
git diff # sanity-check the bumps
|
||||
git add -u
|
||||
git commit -m "chore: release vX.Y.Z"
|
||||
git push origin release/vX.Y.Z
|
||||
```
|
||||
|
||||
Then on GitHub:
|
||||
|
||||
- [ ] Open a PR from `release/vX.Y.Z` into `master`.
|
||||
- [ ] Wait for all required CI checks (build, golangci-lint, spellcheck,
|
||||
doctests, e2e where applicable) to pass.
|
||||
- [ ] Get at least one maintainer approval.
|
||||
- [ ] Merge the PR (use a merge commit — the tag will point at the merge SHA).
|
||||
|
||||
## 3. Tag the release
|
||||
|
||||
After the release PR is merged, pull the latest `master` and dry-run the
|
||||
tagger:
|
||||
|
||||
```shell
|
||||
git checkout master && git pull --ff-only
|
||||
TAG=vX.Y.Z ./scripts/tag.sh vX.Y.Z
|
||||
```
|
||||
|
||||
The script defaults to **dry-run** and prints the commands it would run.
|
||||
Verify the output, then apply for real with `-t`:
|
||||
|
||||
```shell
|
||||
./scripts/tag.sh vX.Y.Z -t
|
||||
```
|
||||
|
||||
This creates and pushes:
|
||||
- The top-level tag `vX.Y.Z`.
|
||||
- A per-module tag `<module>/vX.Y.Z` for each public sub-module
|
||||
(skipping `example/*` and `internal/*`).
|
||||
|
||||
## 4. Publish the GitHub release
|
||||
|
||||
1. On GitHub, open the draft release created by release-drafter.
|
||||
2. Set the tag to `vX.Y.Z` and the target to `master`.
|
||||
3. Replace the auto-generated body with the curated notes from
|
||||
`RELEASE-NOTES.md` for this version.
|
||||
4. For pre-releases, check **"Set as a pre-release"**.
|
||||
5. Publish.
|
||||
|
||||
## 5. Post-release
|
||||
|
||||
- [ ] Verify the release appears on
|
||||
[pkg.go.dev](https://pkg.go.dev/github.com/redis/go-redis/v9) within
|
||||
a few minutes (trigger a fetch by visiting the version URL if needed).
|
||||
- [ ] Announce on Discord (see the link in `CONTRIBUTING.md`).
|
||||
- [ ] Close the release milestone if one was used.
|
||||
- [ ] Open follow-up issues for anything deferred from this release.
|
||||
|
||||
## Hotfix / patch release
|
||||
|
||||
For an urgent fix on top of the latest release:
|
||||
|
||||
1. Branch from the latest release tag: `git checkout -b hotfix/vX.Y.Z+1 vX.Y.Z`.
|
||||
2. Cherry-pick (or re-apply) only the required fix commits.
|
||||
3. Follow the normal release flow above with `TAG=vX.Y.Z+1`.
|
||||
4. Make sure the fix is also present on `master` (forward-port if necessary).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **`release.sh` fails with "tag already exists"** — the tag has already
|
||||
been created. Pick the next version, or delete the local tag first if
|
||||
it was created by mistake.
|
||||
- **`tag.sh` reports version mismatch in a `go.mod`** — a sub-module was
|
||||
not updated by `release.sh`. Fix the `go.mod` manually (or re-run
|
||||
`release.sh`), amend the release PR, and re-run the tagger.
|
||||
- **`version.go` does not contain the tag** — `release.sh` did not run or
|
||||
the bump was reverted. Re-run `release.sh` on the release branch.
|
||||
- **pkg.go.dev does not show the new version** — visit
|
||||
`https://pkg.go.dev/github.com/redis/go-redis/[email protected]` once to trigger
|
||||
a fetch from the module proxy.
|
||||
|
||||
@@ -61,6 +61,13 @@ func (oa *optionsAdapter) GetAddr() string {
|
||||
return oa.options.Addr
|
||||
}
|
||||
|
||||
// GetNodeAddress returns the address of the Redis node as reported by the server.
|
||||
// For cluster clients, this is the endpoint from CLUSTER SLOTS before any transformation.
|
||||
// For standalone clients, this defaults to Addr.
|
||||
func (oa *optionsAdapter) GetNodeAddress() string {
|
||||
return oa.options.NodeAddress
|
||||
}
|
||||
|
||||
// IsTLSEnabled returns true if TLS is enabled.
|
||||
func (oa *optionsAdapter) IsTLSEnabled() bool {
|
||||
return oa.options.TLSConfig != nil
|
||||
|
||||
@@ -0,0 +1,387 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
// note: the APIs is experimental and may be subject to change.
|
||||
//
|
||||
// ArrayCmdable defines the interface for Redis Array data structure commands
|
||||
// available in Redis 8.8.0+.
|
||||
//
|
||||
// Redis array supports index range [0, math.MaxUint64-1), so index parameters use uint64.
|
||||
type ArrayCmdable interface {
|
||||
ARSet(ctx context.Context, key string, index uint64, values ...string) *IntCmd
|
||||
ARGet(ctx context.Context, key string, index uint64) *StringCmd
|
||||
ARGetRange(ctx context.Context, key string, start, end uint64) *SliceCmd
|
||||
ARMGet(ctx context.Context, key string, indexes ...uint64) *SliceCmd
|
||||
ARMSet(ctx context.Context, key string, members ...AREntry) *IntCmd
|
||||
ARInsert(ctx context.Context, key string, values ...string) *UintCmd
|
||||
ARDel(ctx context.Context, key string, indexes ...uint64) *IntCmd
|
||||
ARDelRange(ctx context.Context, key string, ranges ...ARRange) *UintCmd
|
||||
ARLen(ctx context.Context, key string) *UintCmd
|
||||
ARCount(ctx context.Context, key string) *UintCmd
|
||||
ARNext(ctx context.Context, key string) *UintCmd
|
||||
ARSeek(ctx context.Context, key string, index uint64) *IntCmd
|
||||
ARInfo(ctx context.Context, key string) *MapStringInterfaceCmd
|
||||
ARInfoFull(ctx context.Context, key string) *MapStringInterfaceCmd
|
||||
ARScan(ctx context.Context, key string, start, end uint64, args *ARScanArgs) *AREntrySliceCmd
|
||||
AROpSum(ctx context.Context, key string, start, end uint64) *StringCmd
|
||||
AROpMin(ctx context.Context, key string, start, end uint64) *StringCmd
|
||||
AROpMax(ctx context.Context, key string, start, end uint64) *StringCmd
|
||||
AROpAnd(ctx context.Context, key string, start, end uint64) *IntCmd
|
||||
AROpOr(ctx context.Context, key string, start, end uint64) *IntCmd
|
||||
AROpXor(ctx context.Context, key string, start, end uint64) *IntCmd
|
||||
AROpMatch(ctx context.Context, key string, start, end uint64, value string) *IntCmd
|
||||
AROpUsed(ctx context.Context, key string, start, end uint64) *IntCmd
|
||||
ARGrep(ctx context.Context, key string, start, end string, args *ARGrepArgs) *UintSliceCmd
|
||||
ARGrepWithValues(ctx context.Context, key string, start, end string, args *ARGrepArgs) *AREntrySliceCmd
|
||||
ARRing(ctx context.Context, key string, size uint64, values ...string) *UintCmd
|
||||
ARLastItems(ctx context.Context, key string, count uint64, rev bool) *SliceCmd
|
||||
}
|
||||
|
||||
// AREntry represents an index-value pair for ARMSET.
|
||||
type AREntry struct {
|
||||
Index uint64
|
||||
Value string
|
||||
}
|
||||
|
||||
// ARRange represents a start-end range for ARDELRANGE.
|
||||
type ARRange struct {
|
||||
Start uint64
|
||||
End uint64
|
||||
}
|
||||
|
||||
// ARScanArgs contains optional arguments for ARSCAN.
|
||||
type ARScanArgs struct {
|
||||
Limit uint64
|
||||
}
|
||||
|
||||
// ARGrepPredicateType defines the type of predicate for ARGREP.
|
||||
type ARGrepPredicateType string
|
||||
|
||||
const (
|
||||
ARGrepExact ARGrepPredicateType = "EXACT"
|
||||
ARGrepMatch ARGrepPredicateType = "MATCH"
|
||||
ARGrepGlob ARGrepPredicateType = "GLOB"
|
||||
ARGrepRegex ARGrepPredicateType = "RE"
|
||||
)
|
||||
|
||||
// ARGrepPredicate represents a search predicate for ARGREP.
|
||||
type ARGrepPredicate struct {
|
||||
Type ARGrepPredicateType
|
||||
Value string
|
||||
}
|
||||
|
||||
// ARGrepArgs contains optional arguments for ARGREP.
|
||||
// Redis ARGREP defaults to OR when multiple predicates are given.
|
||||
// Set CombineAnd to true to combine predicates with AND instead.
|
||||
type ARGrepArgs struct {
|
||||
Predicates []ARGrepPredicate
|
||||
CombineAnd bool
|
||||
Limit uint64
|
||||
NoCase bool
|
||||
}
|
||||
|
||||
// ARSet sets one or more contiguous values starting at an index in an array.
|
||||
// Returns the number of new slots that were set (previously empty).
|
||||
func (c cmdable) ARSet(ctx context.Context, key string, index uint64, values ...string) *IntCmd {
|
||||
args := make([]any, 3, 3+len(values))
|
||||
args[0] = "arset"
|
||||
args[1] = key
|
||||
args[2] = index
|
||||
for _, v := range values {
|
||||
args = append(args, v)
|
||||
}
|
||||
cmd := NewIntCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// ARGet gets the value at an index in an array.
|
||||
// Returns redis.Nil if the key or index does not exist.
|
||||
func (c cmdable) ARGet(ctx context.Context, key string, index uint64) *StringCmd {
|
||||
cmd := NewStringCmd(ctx, "arget", key, index)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// ARGetRange gets values in a range of indexes.
|
||||
// Returns values in the range, with nil for unset indexes.
|
||||
func (c cmdable) ARGetRange(ctx context.Context, key string, start, end uint64) *SliceCmd {
|
||||
cmd := NewSliceCmd(ctx, "argetrange", key, start, end)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// ARMGet gets values at multiple indexes in an array.
|
||||
// Returns values at the specified indexes, with nil for unset indexes.
|
||||
func (c cmdable) ARMGet(ctx context.Context, key string, indexes ...uint64) *SliceCmd {
|
||||
args := make([]any, 2+len(indexes))
|
||||
args[0] = "armget"
|
||||
args[1] = key
|
||||
for i, idx := range indexes {
|
||||
args[2+i] = idx
|
||||
}
|
||||
cmd := NewSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// ARMSet sets multiple index-value pairs in an array.
|
||||
// Returns the number of new slots that were set (previously empty).
|
||||
func (c cmdable) ARMSet(ctx context.Context, key string, members ...AREntry) *IntCmd {
|
||||
args := make([]any, 2, 2+2*len(members))
|
||||
args[0] = "armset"
|
||||
args[1] = key
|
||||
for _, m := range members {
|
||||
args = append(args, m.Index, m.Value)
|
||||
}
|
||||
cmd := NewIntCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// ARInsert inserts one or more values at consecutive indexes.
|
||||
// Returns the last index where a value was inserted.
|
||||
func (c cmdable) ARInsert(ctx context.Context, key string, values ...string) *UintCmd {
|
||||
args := make([]any, 2, 2+len(values))
|
||||
args[0] = "arinsert"
|
||||
args[1] = key
|
||||
for _, v := range values {
|
||||
args = append(args, v)
|
||||
}
|
||||
cmd := NewUintCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// ARDel deletes elements at the specified indexes in an array.
|
||||
// Returns the number of elements deleted.
|
||||
func (c cmdable) ARDel(ctx context.Context, key string, indexes ...uint64) *IntCmd {
|
||||
args := make([]any, 2+len(indexes))
|
||||
args[0] = "ardel"
|
||||
args[1] = key
|
||||
for i, idx := range indexes {
|
||||
args[2+i] = idx
|
||||
}
|
||||
cmd := NewIntCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// ARDelRange deletes elements in one or more ranges.
|
||||
// Returns the number of elements deleted.
|
||||
func (c cmdable) ARDelRange(ctx context.Context, key string, ranges ...ARRange) *UintCmd {
|
||||
args := make([]any, 2, 2+2*len(ranges))
|
||||
args[0] = "ardelrange"
|
||||
args[1] = key
|
||||
for _, r := range ranges {
|
||||
args = append(args, r.Start, r.End)
|
||||
}
|
||||
cmd := NewUintCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// ARLen returns the length of an array (max index + 1).
|
||||
// Returns 0 if the key does not exist.
|
||||
func (c cmdable) ARLen(ctx context.Context, key string) *UintCmd {
|
||||
cmd := NewUintCmd(ctx, "arlen", key)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// ARCount returns the number of non-empty elements in an array.
|
||||
// Returns 0 if the key does not exist.
|
||||
func (c cmdable) ARCount(ctx context.Context, key string) *UintCmd {
|
||||
cmd := NewUintCmd(ctx, "arcount", key)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// ARNext returns the next index ARINSERT would use.
|
||||
// Returns 0 for missing keys or when no insert happened yet.
|
||||
// Returns nil when the insertion cursor is exhausted / would overflow.
|
||||
func (c cmdable) ARNext(ctx context.Context, key string) *UintCmd {
|
||||
cmd := NewUintCmd(ctx, "arnext", key)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// ARSeek sets the ARINSERT / ARRING cursor to a specific index.
|
||||
// Returns 1 if the cursor was set, 0 if the key does not exist.
|
||||
func (c cmdable) ARSeek(ctx context.Context, key string, index uint64) *IntCmd {
|
||||
cmd := NewIntCmd(ctx, "arseek", key, index)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// ARInfo returns metadata about an array.
|
||||
func (c cmdable) ARInfo(ctx context.Context, key string) *MapStringInterfaceCmd {
|
||||
cmd := NewMapStringInterfaceCmd(ctx, "arinfo", key)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// ARInfoFull returns detailed metadata about an array including slice statistics.
|
||||
func (c cmdable) ARInfoFull(ctx context.Context, key string) *MapStringInterfaceCmd {
|
||||
cmd := NewMapStringInterfaceCmd(ctx, "arinfo", key, "full")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// ARScan iterates existing elements in a range, returning index-value pairs.
|
||||
func (c cmdable) ARScan(ctx context.Context, key string, start, end uint64, scanArgs *ARScanArgs) *AREntrySliceCmd {
|
||||
args := make([]any, 4, 6)
|
||||
args[0], args[1], args[2], args[3] = "arscan", key, start, end
|
||||
if scanArgs != nil && scanArgs.Limit > 0 {
|
||||
args = append(args, "limit", scanArgs.Limit)
|
||||
}
|
||||
cmd := NewAREntrySliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// AROpSum returns the sum of numeric elements in a range.
|
||||
func (c cmdable) AROpSum(ctx context.Context, key string, start, end uint64) *StringCmd {
|
||||
cmd := NewStringCmd(ctx, "arop", key, start, end, "SUM")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// AROpMin returns the minimum numeric element in a range.
|
||||
func (c cmdable) AROpMin(ctx context.Context, key string, start, end uint64) *StringCmd {
|
||||
cmd := NewStringCmd(ctx, "arop", key, start, end, "MIN")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// AROpMax returns the maximum numeric element in a range.
|
||||
func (c cmdable) AROpMax(ctx context.Context, key string, start, end uint64) *StringCmd {
|
||||
cmd := NewStringCmd(ctx, "arop", key, start, end, "MAX")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// AROpAnd returns the bitwise AND of integer elements in a range.
|
||||
func (c cmdable) AROpAnd(ctx context.Context, key string, start, end uint64) *IntCmd {
|
||||
cmd := NewIntCmd(ctx, "arop", key, start, end, "AND")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// AROpOr returns the bitwise OR of integer elements in a range.
|
||||
func (c cmdable) AROpOr(ctx context.Context, key string, start, end uint64) *IntCmd {
|
||||
cmd := NewIntCmd(ctx, "arop", key, start, end, "OR")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// AROpXor returns the bitwise XOR of integer elements in a range.
|
||||
func (c cmdable) AROpXor(ctx context.Context, key string, start, end uint64) *IntCmd {
|
||||
cmd := NewIntCmd(ctx, "arop", key, start, end, "XOR")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// AROpMatch returns the count of elements matching a target string in a range.
|
||||
func (c cmdable) AROpMatch(ctx context.Context, key string, start, end uint64, value string) *IntCmd {
|
||||
cmd := NewIntCmd(ctx, "arop", key, start, end, "MATCH", value)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// AROpUsed returns the count of non-empty slots in a range.
|
||||
func (c cmdable) AROpUsed(ctx context.Context, key string, start, end uint64) *IntCmd {
|
||||
cmd := NewIntCmd(ctx, "arop", key, start, end, "USED")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// ARGrep searches array elements in a range using textual predicates.
|
||||
// Returns matching indexes only. Use ARGrepWithValues to also get the values.
|
||||
func (c cmdable) ARGrep(ctx context.Context, key string, start, end string, grepArgs *ARGrepArgs) *UintSliceCmd {
|
||||
args := make([]any, 4, 4+grepArgs.Len())
|
||||
args[0], args[1], args[2], args[3] = "argrep", key, start, end
|
||||
args = grepArgs.Append(args)
|
||||
cmd := NewUintSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// ARGrepWithValues searches array elements in a range using textual predicates.
|
||||
// Returns matching indexes and their values as index-value pairs.
|
||||
func (c cmdable) ARGrepWithValues(ctx context.Context, key string, start, end string, grepArgs *ARGrepArgs) *AREntrySliceCmd {
|
||||
args := make([]any, 4, 5+grepArgs.Len())
|
||||
args[0], args[1], args[2], args[3] = "argrep", key, start, end
|
||||
args = grepArgs.Append(args)
|
||||
args = append(args, "withvalues")
|
||||
cmd := NewAREntrySliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (args *ARGrepArgs) Len() int {
|
||||
if args == nil {
|
||||
return 0
|
||||
}
|
||||
n := 2 * len(args.Predicates)
|
||||
if args.CombineAnd {
|
||||
n++
|
||||
}
|
||||
if args.Limit > 0 {
|
||||
n += 2
|
||||
}
|
||||
if args.NoCase {
|
||||
n++
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func (args *ARGrepArgs) Append(a []any) []any {
|
||||
if args == nil {
|
||||
return a
|
||||
}
|
||||
for _, p := range args.Predicates {
|
||||
a = append(a, string(p.Type), p.Value)
|
||||
}
|
||||
if args.CombineAnd {
|
||||
a = append(a, "and")
|
||||
}
|
||||
if args.Limit > 0 {
|
||||
a = append(a, "limit", args.Limit)
|
||||
}
|
||||
if args.NoCase {
|
||||
a = append(a, "nocase")
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
// ARRing inserts values into a ring buffer of specified size, wrapping and truncating as needed.
|
||||
// Returns the last index where a value was inserted.
|
||||
func (c cmdable) ARRing(ctx context.Context, key string, size uint64, values ...string) *UintCmd {
|
||||
args := make([]any, 3, 3+len(values))
|
||||
args[0] = "arring"
|
||||
args[1] = key
|
||||
args[2] = size
|
||||
for _, v := range values {
|
||||
args = append(args, v)
|
||||
}
|
||||
cmd := NewUintCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// ARLastItems returns the most recently inserted elements.
|
||||
// When rev is true, returns items in reverse order.
|
||||
func (c cmdable) ARLastItems(ctx context.Context, key string, count uint64, rev bool) *SliceCmd {
|
||||
args := make([]any, 3, 4)
|
||||
args[0], args[1], args[2] = "arlastitems", key, count
|
||||
if rev {
|
||||
args = append(args, "rev")
|
||||
}
|
||||
cmd := NewSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,12 +9,30 @@ type StreamingCredentialsProvider interface {
|
||||
// Subscribe subscribes to the credentials provider for updates.
|
||||
// It returns the current credentials, a cancel function to unsubscribe from the provider,
|
||||
// and an error if any.
|
||||
//
|
||||
// Implementations MUST be idempotent with respect to listener identity:
|
||||
// subscribing the same listener value more than once must not produce
|
||||
// duplicate notifications and must not create multiple independent
|
||||
// subscriptions that each need to be cancelled separately. Every
|
||||
// UnsubscribeFunc returned for a given listener must cancel that
|
||||
// listener's subscription; calling any one of them must be sufficient to
|
||||
// stop updates to that listener, and calling subsequent ones must be a
|
||||
// safe no-op. Callers (including go-redis internals) may retain only
|
||||
// the most recently returned UnsubscribeFunc and rely on it to fully
|
||||
// unsubscribe the listener.
|
||||
//
|
||||
// TODO(ndyakov): Should we add context to the Subscribe method?
|
||||
Subscribe(listener CredentialsListener) (Credentials, UnsubscribeFunc, error)
|
||||
}
|
||||
|
||||
// UnsubscribeFunc is a function that is used to cancel the subscription to the credentials provider.
|
||||
// It is used to unsubscribe from the provider when the credentials are no longer needed.
|
||||
//
|
||||
// Per the StreamingCredentialsProvider.Subscribe contract, if the same
|
||||
// listener is subscribed multiple times, every UnsubscribeFunc returned for
|
||||
// that listener must fully unsubscribe it on first invocation, and
|
||||
// subsequent invocations (from any of the equivalent UnsubscribeFuncs) must
|
||||
// be a safe no-op.
|
||||
type UnsubscribeFunc func() error
|
||||
|
||||
// CredentialsListener is an interface that defines the methods for a credentials listener.
|
||||
|
||||
+1431
-27
File diff suppressed because it is too large
Load Diff
+447
@@ -1,7 +1,9 @@
|
||||
package redis_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/hanzoai/kv-go/v9"
|
||||
@@ -10,6 +12,35 @@ import (
|
||||
. "github.com/bsm/gomega"
|
||||
)
|
||||
|
||||
// parseRESPType returns the RESP type character and validates basic structure
|
||||
func parseRESPType(data []byte) (byte, bool) {
|
||||
if len(data) < 3 {
|
||||
return 0, false
|
||||
}
|
||||
// Check for CRLF at the end
|
||||
if !bytes.HasSuffix(data, []byte("\r\n")) {
|
||||
return 0, false
|
||||
}
|
||||
return data[0], true
|
||||
}
|
||||
|
||||
// countRESPElements counts the number of elements in a RESP array/map response
|
||||
func countRESPElements(data []byte) (int, bool) {
|
||||
if len(data) < 4 {
|
||||
return 0, false
|
||||
}
|
||||
// Find first CRLF
|
||||
idx := bytes.Index(data, []byte("\r\n"))
|
||||
if idx < 2 {
|
||||
return 0, false
|
||||
}
|
||||
count, err := strconv.Atoi(string(data[1:idx]))
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
return count, true
|
||||
}
|
||||
|
||||
var _ = Describe("Cmd", func() {
|
||||
var client *redis.Client
|
||||
|
||||
@@ -94,3 +125,419 @@ var _ = Describe("Cmd", func() {
|
||||
Expect(err).To(Equal(e))
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("RawCmd", func() {
|
||||
var client *redis.Client
|
||||
|
||||
BeforeEach(func() {
|
||||
client = redis.NewClient(redisOptions())
|
||||
Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
Expect(client.Close()).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
It("returns raw RESP bytes for simple string (PING)", func() {
|
||||
cmd := client.DoRaw(ctx, "PING")
|
||||
Expect(cmd.Err()).NotTo(HaveOccurred())
|
||||
rawBytes, err := cmd.Result()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(string(rawBytes)).To(Equal("+PONG\r\n"))
|
||||
})
|
||||
|
||||
It("returns raw RESP bytes for bulk string (GET)", func() {
|
||||
err := client.Set(ctx, "rawtest", "hello", 0).Err()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
cmd := client.DoRaw(ctx, "GET", "rawtest")
|
||||
Expect(cmd.Err()).NotTo(HaveOccurred())
|
||||
rawBytes, err := cmd.Result()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(string(rawBytes)).To(Equal("$5\r\nhello\r\n"))
|
||||
})
|
||||
|
||||
It("returns raw RESP bytes for integer (INCR)", func() {
|
||||
err := client.Set(ctx, "counter", "10", 0).Err()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
cmd := client.DoRaw(ctx, "INCR", "counter")
|
||||
Expect(cmd.Err()).NotTo(HaveOccurred())
|
||||
rawBytes, err := cmd.Result()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(string(rawBytes)).To(Equal(":11\r\n"))
|
||||
})
|
||||
|
||||
It("returns raw RESP bytes for array (KEYS) with validation", func() {
|
||||
err := client.Set(ctx, "key1", "val1", 0).Err()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
err = client.Set(ctx, "key2", "val2", 0).Err()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
cmd := client.DoRaw(ctx, "KEYS", "key*")
|
||||
Expect(cmd.Err()).NotTo(HaveOccurred())
|
||||
rawBytes, err := cmd.Result()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Validate RESP structure
|
||||
respType, valid := parseRESPType(rawBytes)
|
||||
Expect(valid).To(BeTrue())
|
||||
Expect(respType).To(Equal(byte('*')), "Should be array type")
|
||||
|
||||
// Verify element count
|
||||
count, ok := countRESPElements(rawBytes)
|
||||
Expect(ok).To(BeTrue())
|
||||
Expect(count).To(Equal(2))
|
||||
|
||||
// Verify keys are present in the raw bytes
|
||||
rawStr := string(rawBytes)
|
||||
Expect(rawStr).To(ContainSubstring("key1"))
|
||||
Expect(rawStr).To(ContainSubstring("key2"))
|
||||
})
|
||||
|
||||
It("returns raw RESP bytes for HGETALL with complete validation", func() {
|
||||
err := client.HSet(ctx, "myhash", "field1", "value1", "field2", "value2").Err()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
cmd := client.DoRaw(ctx, "HGETALL", "myhash")
|
||||
Expect(cmd.Err()).NotTo(HaveOccurred())
|
||||
rawBytes, err := cmd.Result()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Validate RESP structure
|
||||
respType, valid := parseRESPType(rawBytes)
|
||||
Expect(valid).To(BeTrue(), "Response should be valid RESP")
|
||||
|
||||
// HGETALL returns array (RESP2) or map (RESP3)
|
||||
Expect(respType).To(BeElementOf(byte('*'), byte('%')), "Should be array or map type")
|
||||
|
||||
// Count elements: array has 4 elements (2 key-value pairs), map has 2 pairs
|
||||
count, ok := countRESPElements(rawBytes)
|
||||
Expect(ok).To(BeTrue())
|
||||
if respType == '*' {
|
||||
Expect(count).To(Equal(4), "Array should have 4 elements (2 key-value pairs)")
|
||||
} else {
|
||||
Expect(count).To(Equal(2), "Map should have 2 pairs")
|
||||
}
|
||||
|
||||
rawStr := string(rawBytes)
|
||||
Expect(rawStr).To(ContainSubstring("field1"))
|
||||
Expect(rawStr).To(ContainSubstring("value1"))
|
||||
Expect(rawStr).To(ContainSubstring("field2"))
|
||||
Expect(rawStr).To(ContainSubstring("value2"))
|
||||
|
||||
// Verify response ends with CRLF
|
||||
Expect(rawBytes[len(rawBytes)-2:]).To(Equal([]byte("\r\n")))
|
||||
})
|
||||
|
||||
It("returns raw RESP bytes for nil value", func() {
|
||||
cmd := client.DoRaw(ctx, "GET", "nonexistent")
|
||||
Expect(cmd.Err()).NotTo(HaveOccurred())
|
||||
rawBytes, err := cmd.Result()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
rawStr := string(rawBytes)
|
||||
Expect(rawStr == "$-1\r\n" || rawStr == "_\r\n").To(BeTrue())
|
||||
})
|
||||
|
||||
It("returns raw RESP bytes for empty array", func() {
|
||||
cmd := client.DoRaw(ctx, "KEYS", "nonexistent*")
|
||||
Expect(cmd.Err()).NotTo(HaveOccurred())
|
||||
rawBytes, err := cmd.Result()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(string(rawBytes)).To(Equal("*0\r\n"))
|
||||
})
|
||||
|
||||
It("has Val and Bytes methods", func() {
|
||||
cmd := client.DoRaw(ctx, "PING")
|
||||
Expect(cmd.Err()).NotTo(HaveOccurred())
|
||||
|
||||
val := cmd.Val()
|
||||
Expect(string(val)).To(Equal("+PONG\r\n"))
|
||||
|
||||
bytes, err := cmd.Bytes()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(string(bytes)).To(Equal("+PONG\r\n"))
|
||||
})
|
||||
|
||||
It("implements Stringer", func() {
|
||||
cmd := client.DoRaw(ctx, "PING")
|
||||
Expect(cmd.Err()).NotTo(HaveOccurred())
|
||||
Expect(cmd.String()).To(ContainSubstring("PING"))
|
||||
})
|
||||
|
||||
It("allows to set custom error", func() {
|
||||
e := errors.New("custom error")
|
||||
cmd := redis.RawCmd{}
|
||||
cmd.SetErr(e)
|
||||
_, err := cmd.Result()
|
||||
Expect(err).To(Equal(e))
|
||||
})
|
||||
|
||||
It("can be cloned", func() {
|
||||
cmd := client.DoRaw(ctx, "PING")
|
||||
Expect(cmd.Err()).NotTo(HaveOccurred())
|
||||
|
||||
cloned := cmd.Clone().(*redis.RawCmd)
|
||||
Expect(cloned.Val()).To(Equal(cmd.Val()))
|
||||
originalVal := cmd.Val()
|
||||
clonedVal := cloned.Val()
|
||||
Expect(&originalVal[0]).NotTo(BeIdenticalTo(&clonedVal[0]))
|
||||
})
|
||||
|
||||
It("returns raw RESP bytes for error response", func() {
|
||||
cmd := client.DoRaw(ctx, "INVALIDCMD")
|
||||
rawBytes, err := cmd.Result()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(string(rawBytes)).To(HavePrefix("-"))
|
||||
Expect(string(rawBytes)).To(ContainSubstring("ERR"))
|
||||
})
|
||||
|
||||
It("returns raw RESP bytes for empty hash (nil map)", func() {
|
||||
// HGETALL on non-existent hash returns empty array/map
|
||||
cmd := client.DoRaw(ctx, "HGETALL", "nonexistent_hash")
|
||||
Expect(cmd.Err()).NotTo(HaveOccurred())
|
||||
rawBytes, err := cmd.Result()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
respType, valid := parseRESPType(rawBytes)
|
||||
Expect(valid).To(BeTrue())
|
||||
// Should be empty array or empty map
|
||||
Expect(respType).To(BeElementOf(byte('*'), byte('%')))
|
||||
|
||||
count, ok := countRESPElements(rawBytes)
|
||||
Expect(ok).To(BeTrue())
|
||||
Expect(count).To(Equal(0), "Empty hash should return 0 elements")
|
||||
})
|
||||
|
||||
It("returns raw RESP bytes for nested array structures (SMEMBERS)", func() {
|
||||
// Create a set with multiple members
|
||||
err := client.SAdd(ctx, "myset", "member1", "member2", "member3").Err()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
cmd := client.DoRaw(ctx, "SMEMBERS", "myset")
|
||||
Expect(cmd.Err()).NotTo(HaveOccurred())
|
||||
rawBytes, err := cmd.Result()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Validate basic RESP structure
|
||||
respType, valid := parseRESPType(rawBytes)
|
||||
Expect(valid).To(BeTrue())
|
||||
// SMEMBERS returns array (RESP2) or set (RESP3)
|
||||
Expect(respType).To(BeElementOf(byte('*'), byte('~')))
|
||||
|
||||
// Verify count
|
||||
count, ok := countRESPElements(rawBytes)
|
||||
Expect(ok).To(BeTrue())
|
||||
Expect(count).To(Equal(3))
|
||||
|
||||
// Verify members are present
|
||||
rawStr := string(rawBytes)
|
||||
Expect(rawStr).To(ContainSubstring("member1"))
|
||||
Expect(rawStr).To(ContainSubstring("member2"))
|
||||
Expect(rawStr).To(ContainSubstring("member3"))
|
||||
})
|
||||
|
||||
It("returns raw RESP bytes for LRANGE with exact byte validation", func() {
|
||||
// Create a list with multiple elements
|
||||
err := client.RPush(ctx, "mylist", "elem1", "elem2", "elem3").Err()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
cmd := client.DoRaw(ctx, "LRANGE", "mylist", "0", "-1")
|
||||
Expect(cmd.Err()).NotTo(HaveOccurred())
|
||||
rawBytes, err := cmd.Result()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// LRANGE returns elements in order, so we can validate exact bytes
|
||||
expectedResp := "*3\r\n$5\r\nelem1\r\n$5\r\nelem2\r\n$5\r\nelem3\r\n"
|
||||
Expect(string(rawBytes)).To(Equal(expectedResp))
|
||||
})
|
||||
|
||||
It("returns raw RESP bytes for LPUSH with exact integer response", func() {
|
||||
// LPUSH returns the length of the list after push
|
||||
cmd := client.DoRaw(ctx, "LPUSH", "newlist", "item")
|
||||
Expect(cmd.Err()).NotTo(HaveOccurred())
|
||||
rawBytes, err := cmd.Result()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(string(rawBytes)).To(Equal(":1\r\n"))
|
||||
})
|
||||
|
||||
It("returns raw RESP bytes for ECHO with exact bulk string", func() {
|
||||
cmd := client.DoRaw(ctx, "ECHO", "testmsg")
|
||||
Expect(cmd.Err()).NotTo(HaveOccurred())
|
||||
rawBytes, err := cmd.Result()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(string(rawBytes)).To(Equal("$7\r\ntestmsg\r\n"))
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("RawWriteToCmd", func() {
|
||||
var client *redis.Client
|
||||
|
||||
BeforeEach(func() {
|
||||
client = redis.NewClient(redisOptions())
|
||||
Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
Expect(client.Close()).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
It("streams raw RESP bytes for PING", func() {
|
||||
var buf bytes.Buffer
|
||||
cmd := client.DoRawWriteTo(ctx, &buf, "PING")
|
||||
written, err := cmd.Result()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(written).To(Equal(int64(7)))
|
||||
Expect(buf.String()).To(Equal("+PONG\r\n"))
|
||||
})
|
||||
|
||||
It("streams raw RESP bytes for bulk string (GET)", func() {
|
||||
client.Set(ctx, "rawkey", "rawvalue", 0)
|
||||
var buf bytes.Buffer
|
||||
cmd := client.DoRawWriteTo(ctx, &buf, "GET", "rawkey")
|
||||
written, err := cmd.Result()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(written).To(BeNumerically(">", 0))
|
||||
Expect(buf.String()).To(Equal("$8\r\nrawvalue\r\n"))
|
||||
})
|
||||
|
||||
It("streams raw RESP bytes for integer (INCR)", func() {
|
||||
client.Set(ctx, "writeto_counter", "10", 0)
|
||||
var buf bytes.Buffer
|
||||
cmd := client.DoRawWriteTo(ctx, &buf, "INCR", "writeto_counter")
|
||||
written, err := cmd.Result()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(written).To(Equal(int64(5)))
|
||||
Expect(buf.String()).To(Equal(":11\r\n"))
|
||||
})
|
||||
|
||||
It("streams raw RESP bytes for nil value (GET non-existent)", func() {
|
||||
var buf bytes.Buffer
|
||||
cmd := client.DoRawWriteTo(ctx, &buf, "GET", "nonexistent_key")
|
||||
written, err := cmd.Result()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(written).To(Equal(int64(buf.Len())))
|
||||
rawStr := buf.String()
|
||||
// RESP2 returns $-1\r\n, RESP3 returns _\r\n
|
||||
Expect(rawStr == "$-1\r\n" || rawStr == "_\r\n").To(BeTrue())
|
||||
})
|
||||
|
||||
It("streams raw RESP bytes for array (LRANGE) with exact validation", func() {
|
||||
client.RPush(ctx, "writeto_list", "a", "b", "c")
|
||||
var buf bytes.Buffer
|
||||
cmd := client.DoRawWriteTo(ctx, &buf, "LRANGE", "writeto_list", "0", "-1")
|
||||
written, err := cmd.Result()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(written).To(Equal(int64(buf.Len())))
|
||||
// LRANGE returns elements in order
|
||||
expectedResp := "*3\r\n$1\r\na\r\n$1\r\nb\r\n$1\r\nc\r\n"
|
||||
Expect(buf.String()).To(Equal(expectedResp))
|
||||
})
|
||||
|
||||
It("streams raw RESP bytes for empty array (LRANGE empty list)", func() {
|
||||
var buf bytes.Buffer
|
||||
cmd := client.DoRawWriteTo(ctx, &buf, "LRANGE", "nonexistent_list", "0", "-1")
|
||||
written, err := cmd.Result()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(written).To(Equal(int64(buf.Len())))
|
||||
Expect(buf.String()).To(Equal("*0\r\n"))
|
||||
})
|
||||
|
||||
It("streams raw RESP bytes for HGETALL", func() {
|
||||
client.HSet(ctx, "rawhash", "f1", "v1", "f2", "v2")
|
||||
var buf bytes.Buffer
|
||||
cmd := client.DoRawWriteTo(ctx, &buf, "HGETALL", "rawhash")
|
||||
written, err := cmd.Result()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(written).To(BeNumerically(">", 0))
|
||||
raw := buf.String()
|
||||
Expect(raw).To(ContainSubstring("f1"))
|
||||
Expect(raw).To(ContainSubstring("v1"))
|
||||
})
|
||||
|
||||
It("returns correct byte count", func() {
|
||||
var buf bytes.Buffer
|
||||
cmd := client.DoRawWriteTo(ctx, &buf, "PING")
|
||||
written, err := cmd.Result()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(written).To(Equal(int64(buf.Len())))
|
||||
})
|
||||
|
||||
It("implements Stringer", func() {
|
||||
var buf bytes.Buffer
|
||||
cmd := client.DoRawWriteTo(ctx, &buf, "PING")
|
||||
Expect(cmd.String()).To(ContainSubstring("PING"))
|
||||
})
|
||||
|
||||
It("can be cloned", func() {
|
||||
var buf bytes.Buffer
|
||||
cmd := client.DoRawWriteTo(ctx, &buf, "PING")
|
||||
clone := cmd.Clone().(*redis.RawWriteToCmd)
|
||||
Expect(clone.Val()).To(Equal(cmd.Val()))
|
||||
})
|
||||
|
||||
It("streams HGETALL with complete validation", func() {
|
||||
client.HSet(ctx, "rawhash2", "key1", "val1", "key2", "val2")
|
||||
var buf bytes.Buffer
|
||||
cmd := client.DoRawWriteTo(ctx, &buf, "HGETALL", "rawhash2")
|
||||
written, err := cmd.Result()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Verify written bytes match buffer length
|
||||
Expect(written).To(Equal(int64(buf.Len())))
|
||||
|
||||
// Validate RESP structure
|
||||
rawBytes := buf.Bytes()
|
||||
respType, valid := parseRESPType(rawBytes)
|
||||
Expect(valid).To(BeTrue())
|
||||
Expect(respType).To(BeElementOf(byte('*'), byte('%')))
|
||||
|
||||
// Verify all data present
|
||||
rawStr := buf.String()
|
||||
Expect(rawStr).To(ContainSubstring("key1"))
|
||||
Expect(rawStr).To(ContainSubstring("val1"))
|
||||
Expect(rawStr).To(ContainSubstring("key2"))
|
||||
Expect(rawStr).To(ContainSubstring("val2"))
|
||||
})
|
||||
|
||||
It("streams empty hash correctly", func() {
|
||||
var buf bytes.Buffer
|
||||
cmd := client.DoRawWriteTo(ctx, &buf, "HGETALL", "nonexistent_hash")
|
||||
written, err := cmd.Result()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(written).To(Equal(int64(buf.Len())))
|
||||
|
||||
rawBytes := buf.Bytes()
|
||||
respType, valid := parseRESPType(rawBytes)
|
||||
Expect(valid).To(BeTrue())
|
||||
Expect(respType).To(BeElementOf(byte('*'), byte('%')))
|
||||
|
||||
count, ok := countRESPElements(rawBytes)
|
||||
Expect(ok).To(BeTrue())
|
||||
Expect(count).To(Equal(0))
|
||||
})
|
||||
|
||||
It("streams error response correctly", func() {
|
||||
var buf bytes.Buffer
|
||||
cmd := client.DoRawWriteTo(ctx, &buf, "INVALIDCMD")
|
||||
_, err := cmd.Result()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
rawStr := buf.String()
|
||||
Expect(rawStr).To(HavePrefix("-"))
|
||||
Expect(rawStr).To(ContainSubstring("ERR"))
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("NoRetry behavior", func() {
|
||||
It("RawCmd does not have NoRetry set", func() {
|
||||
cmd := redis.NewRawCmd(ctx, "PING")
|
||||
Expect(cmd.NoRetry()).To(BeFalse())
|
||||
})
|
||||
|
||||
It("RawWriteToCmd has NoRetry set to prevent data corruption", func() {
|
||||
var buf bytes.Buffer
|
||||
cmd := redis.NewRawWriteToCmd(ctx, &buf, "PING")
|
||||
Expect(cmd.NoRetry()).To(BeTrue())
|
||||
})
|
||||
})
|
||||
|
||||
+28
@@ -55,6 +55,11 @@ func appendArgs(dst, src []interface{}) []interface{} {
|
||||
return appendArg(dst, src[0])
|
||||
}
|
||||
|
||||
if cap(dst) < len(dst)+len(src) {
|
||||
newDst := make([]interface{}, len(dst), len(dst)+len(src))
|
||||
copy(newDst, dst)
|
||||
dst = newDst
|
||||
}
|
||||
dst = append(dst, src...)
|
||||
return dst
|
||||
}
|
||||
@@ -210,6 +215,7 @@ type Cmdable interface {
|
||||
ShutdownSave(ctx context.Context) *StatusCmd
|
||||
ShutdownNoSave(ctx context.Context) *StatusCmd
|
||||
SlaveOf(ctx context.Context, host, port string) *StatusCmd
|
||||
ReplicaOf(ctx context.Context, host, port string) *StatusCmd
|
||||
SlowLogGet(ctx context.Context, num int64) *SlowLogCmd
|
||||
SlowLogLen(ctx context.Context) *IntCmd
|
||||
SlowLogReset(ctx context.Context) *StatusCmd
|
||||
@@ -222,6 +228,7 @@ type Cmdable interface {
|
||||
ModuleLoadex(ctx context.Context, conf *ModuleLoadexConfig) *StringCmd
|
||||
|
||||
ACLCmdable
|
||||
ArrayCmdable
|
||||
BitMapCmdable
|
||||
ClusterCmdable
|
||||
GenericCmdable
|
||||
@@ -443,6 +450,20 @@ func (c cmdable) Do(ctx context.Context, args ...interface{}) *Cmd {
|
||||
return cmd
|
||||
}
|
||||
|
||||
// DoRaw executes a command and returns the raw RESP protocol bytes without parsing.
|
||||
func (c cmdable) DoRaw(ctx context.Context, args ...interface{}) *RawCmd {
|
||||
cmd := NewRawCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// DoRawWriteTo executes a command and streams raw RESP bytes directly to w without intermediate allocations.
|
||||
func (c cmdable) DoRawWriteTo(ctx context.Context, w io.Writer, args ...interface{}) *RawWriteToCmd {
|
||||
cmd := NewRawWriteToCmd(ctx, w, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Quit closes the connection.
|
||||
//
|
||||
// Deprecated: Just close the connection instead as of Redis 7.2.0.
|
||||
@@ -677,6 +698,13 @@ func (c cmdable) SlaveOf(ctx context.Context, host, port string) *StatusCmd {
|
||||
return cmd
|
||||
}
|
||||
|
||||
// ReplicaOf sets a Redis server as a replica of another, or promotes it to being a master.
|
||||
func (c cmdable) ReplicaOf(ctx context.Context, host, port string) *StatusCmd {
|
||||
cmd := NewStatusCmd(ctx, "replicaof", host, port)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) SlowLogGet(ctx context.Context, num int64) *SlowLogCmd {
|
||||
cmd := NewSlowLogCmd(context.Background(), "slowlog", "get", num)
|
||||
_ = c(ctx, cmd)
|
||||
|
||||
+871
-41
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,39 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9/internal"
|
||||
)
|
||||
|
||||
// DialRetryBackoffConstant returns a dial retry backoff function that always returns d.
|
||||
// attempt is 0-based: attempt=0 is the delay after the 1st failed dial.
|
||||
func DialRetryBackoffConstant(d time.Duration) func(attempt int) time.Duration {
|
||||
if d < 0 {
|
||||
d = 0
|
||||
}
|
||||
return func(int) time.Duration { return d }
|
||||
}
|
||||
|
||||
// DialRetryBackoffExponential returns a dial retry backoff function that uses exponential
|
||||
// backoff with jitter and a cap, using internal.RetryBackoff.
|
||||
//
|
||||
// attempt is 0-based: attempt=0 is the delay after the 1st failed dial.
|
||||
func DialRetryBackoffExponential(minBackoff, maxBackoff time.Duration) func(attempt int) time.Duration {
|
||||
if minBackoff < 0 {
|
||||
minBackoff = 0
|
||||
}
|
||||
if maxBackoff < 0 {
|
||||
maxBackoff = 0
|
||||
}
|
||||
if minBackoff > maxBackoff {
|
||||
minBackoff = maxBackoff
|
||||
}
|
||||
return func(attempt int) time.Duration {
|
||||
// internal.RetryBackoff expects retry >= 0.
|
||||
if attempt < 0 {
|
||||
attempt = 0
|
||||
}
|
||||
return internal.RetryBackoff(attempt, minBackoff, maxBackoff)
|
||||
}
|
||||
}
|
||||
+78
-8
@@ -1,12 +1,16 @@
|
||||
---
|
||||
|
||||
x-default-image: &default-image ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:8.8.0}
|
||||
|
||||
services:
|
||||
redis:
|
||||
image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:8.4.0}
|
||||
image: *default-image
|
||||
platform: linux/amd64
|
||||
container_name: redis-standalone
|
||||
environment:
|
||||
- TLS_ENABLED=yes
|
||||
- TLS_CLIENT_CNS=testcertuser
|
||||
- TLS_AUTH_CLIENTS_USER=CN
|
||||
- REDIS_CLUSTER=no
|
||||
- PORT=6379
|
||||
- TLS_PORT=6666
|
||||
@@ -21,9 +25,10 @@ services:
|
||||
- sentinel
|
||||
- all-stack
|
||||
- all
|
||||
- e2e
|
||||
|
||||
osscluster:
|
||||
image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:8.4.0}
|
||||
image: *default-image
|
||||
platform: linux/amd64
|
||||
container_name: redis-osscluster
|
||||
environment:
|
||||
@@ -39,14 +44,77 @@ services:
|
||||
- all-stack
|
||||
- all
|
||||
|
||||
cae-resp-proxy:
|
||||
image: redislabs/client-resp-proxy:latest
|
||||
container_name: cae-resp-proxy
|
||||
environment:
|
||||
- TARGET_HOST=redis
|
||||
- TARGET_PORT=6379
|
||||
- LISTEN_PORT=17000,17001,17002,17003 # 4 proxy nodes: initially show 3, swap in 4th during SMIGRATED
|
||||
- LISTEN_HOST=0.0.0.0
|
||||
- API_PORT=3000
|
||||
- DEFAULT_INTERCEPTORS=cluster,hitless
|
||||
ports:
|
||||
- "17000:17000" # Proxy node 1 (host:container)
|
||||
- "17001:17001" # Proxy node 2 (host:container)
|
||||
- "17002:17002" # Proxy node 3 (host:container)
|
||||
- "17003:17003" # Proxy node 4 (host:container) - hidden initially, swapped in during SMIGRATED
|
||||
- "18100:3000" # HTTP API port (host:container)
|
||||
depends_on:
|
||||
- redis
|
||||
profiles:
|
||||
- e2e
|
||||
- all
|
||||
|
||||
proxy-fault-injector:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: maintnotifications/e2e/cmd/proxy-fi-server/Dockerfile
|
||||
container_name: proxy-fault-injector
|
||||
ports:
|
||||
- "15000:5000" # Fault injector API port (host:container)
|
||||
depends_on:
|
||||
- cae-resp-proxy
|
||||
environment:
|
||||
- PROXY_API_URL=http://cae-resp-proxy:3000
|
||||
profiles:
|
||||
- e2e
|
||||
- all
|
||||
|
||||
osscluster-tls:
|
||||
image: *default-image
|
||||
platform: linux/amd64
|
||||
container_name: redis-osscluster-tls
|
||||
environment:
|
||||
- NODES=6
|
||||
- PORT=6430
|
||||
- TLS_PORT=5430
|
||||
- TLS_ENABLED=yes
|
||||
- TLS_CLIENT_CNS=testcertuser
|
||||
- TLS_AUTH_CLIENTS_USER=CN
|
||||
- REDIS_CLUSTER=yes
|
||||
- REPLICAS=1
|
||||
command: "--tls-auth-clients optional --cluster-announce-ip 127.0.0.1"
|
||||
ports:
|
||||
- "6430-6435:6430-6435" # Regular ports
|
||||
- "5430-5435:5430-5435" # TLS ports (set via TLS_PORT env var)
|
||||
- "16430-16435:16430-16435" # Cluster bus ports (PORT + 10000)
|
||||
volumes:
|
||||
- "./dockers/osscluster-tls:/redis/work"
|
||||
profiles:
|
||||
- cluster-tls
|
||||
- all
|
||||
|
||||
sentinel-cluster:
|
||||
image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:8.4.0}
|
||||
image: *default-image
|
||||
platform: linux/amd64
|
||||
container_name: redis-sentinel-cluster
|
||||
network_mode: "host"
|
||||
environment:
|
||||
- NODES=3
|
||||
- TLS_ENABLED=yes
|
||||
- TLS_CLIENT_CNS=testcertuser
|
||||
- TLS_AUTH_CLIENTS_USER=CN
|
||||
- REDIS_CLUSTER=no
|
||||
- PORT=9121
|
||||
command: ${REDIS_EXTRA_ARGS:---enable-debug-command yes --enable-module-command yes --tls-auth-clients optional --save ""}
|
||||
@@ -60,7 +128,7 @@ services:
|
||||
- all
|
||||
|
||||
sentinel:
|
||||
image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:8.4.0}
|
||||
image: *default-image
|
||||
platform: linux/amd64
|
||||
container_name: redis-sentinel
|
||||
depends_on:
|
||||
@@ -84,19 +152,21 @@ services:
|
||||
- all
|
||||
|
||||
ring-cluster:
|
||||
image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:8.4.0}
|
||||
image: *default-image
|
||||
platform: linux/amd64
|
||||
container_name: redis-ring-cluster
|
||||
environment:
|
||||
- NODES=3
|
||||
- TLS_ENABLED=yes
|
||||
- TLS_CLIENT_CNS=testcertuser
|
||||
- TLS_AUTH_CLIENTS_USER=CN
|
||||
- REDIS_CLUSTER=no
|
||||
- PORT=6390
|
||||
command: ${REDIS_EXTRA_ARGS:---enable-debug-command yes --enable-module-command yes --tls-auth-clients optional --save ""}
|
||||
ports:
|
||||
- 6390:6390
|
||||
- 6391:6391
|
||||
- 6392:6392
|
||||
- "6390:6390"
|
||||
- "6391:6391"
|
||||
- "6392:6392"
|
||||
volumes:
|
||||
- "./dockers/ring:/redis/work"
|
||||
profiles:
|
||||
|
||||
@@ -199,11 +199,11 @@ func ExampleClient_geoindex() {
|
||||
// OK
|
||||
// OK
|
||||
// OK
|
||||
// {1 [{product:46885 <nil> <nil> <nil> map[$:{"city":"Denver","description":"Navy Blue Slippers","location":"-104.991531, 39.742043","price":45.99}] <nil>}]}
|
||||
// {1 [{product:46885 <nil> <nil> <nil> map[$:{"city":"Denver","description":"Navy Blue Slippers","location":"-104.991531, 39.742043","price":45.99}] <nil>}] []}
|
||||
// OK
|
||||
// OK
|
||||
// OK
|
||||
// OK
|
||||
// OK
|
||||
// {1 [{shape:4 <nil> <nil> <nil> map[$:[{"geom":"POINT (2 2)","name":"Purple Point"}]] <nil>}]}
|
||||
// {1 [{shape:4 <nil> <nil> <nil> map[$:[{"geom":"POINT (2 2)","name":"Purple Point"}]] <nil>}] []}
|
||||
}
|
||||
|
||||
@@ -219,7 +219,7 @@ func ExampleClient_search_json() {
|
||||
// STEP_END
|
||||
|
||||
// Output:
|
||||
// {1 [{user:3 <nil> <nil> <nil> map[$:{"age":35,"city":"Tel Aviv","email":"[email protected]","name":"Paul Zamir"}] <nil>}]}
|
||||
// {1 [{user:3 <nil> <nil> <nil> map[$:{"age":35,"city":"Tel Aviv","email":"[email protected]","name":"Paul Zamir"}] <nil>}] []}
|
||||
// London
|
||||
// Tel Aviv
|
||||
// 0
|
||||
@@ -329,5 +329,5 @@ func ExampleClient_search_hash() {
|
||||
// STEP_END
|
||||
|
||||
// Output:
|
||||
// {1 [{huser:3 <nil> <nil> <nil> map[age:35 city:Tel Aviv email:[email protected] name:Paul Zamir] <nil>}]}
|
||||
// {1 [{huser:3 <nil> <nil> <nil> map[age:35 city:Tel Aviv email:[email protected] name:Paul Zamir] <nil>}] []}
|
||||
}
|
||||
|
||||
+20
-30
@@ -228,15 +228,13 @@ func ExampleClient_query_agg() {
|
||||
"idx:bicycle",
|
||||
"@condition:{new}",
|
||||
&redis.FTAggregateOptions{
|
||||
Apply: []redis.FTAggregateApply{
|
||||
{
|
||||
Steps: []redis.FTAggregateStep{
|
||||
{Load: &redis.FTAggregateLoad{Field: "__key"}},
|
||||
{Load: &redis.FTAggregateLoad{Field: "price"}},
|
||||
{Apply: &redis.FTAggregateApply{
|
||||
Field: "@price - (@price * 0.1)",
|
||||
As: "discounted",
|
||||
},
|
||||
},
|
||||
Load: []redis.FTAggregateLoad{
|
||||
{Field: "__key"},
|
||||
{Field: "price"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
).Result()
|
||||
@@ -271,17 +269,13 @@ func ExampleClient_query_agg() {
|
||||
res2, err := rdb.FTAggregateWithArgs(ctx,
|
||||
"idx:bicycle", "*",
|
||||
&redis.FTAggregateOptions{
|
||||
Load: []redis.FTAggregateLoad{
|
||||
{Field: "price"},
|
||||
},
|
||||
Apply: []redis.FTAggregateApply{
|
||||
{
|
||||
Steps: []redis.FTAggregateStep{
|
||||
{Load: &redis.FTAggregateLoad{Field: "price"}},
|
||||
{Apply: &redis.FTAggregateApply{
|
||||
Field: "@price<1000",
|
||||
As: "price_category",
|
||||
},
|
||||
},
|
||||
GroupBy: []redis.FTAggregateGroupBy{
|
||||
{
|
||||
}},
|
||||
{GroupBy: &redis.FTAggregateGroupBy{
|
||||
Fields: []interface{}{"@condition"},
|
||||
Reduce: []redis.FTAggregateReducer{
|
||||
{
|
||||
@@ -290,7 +284,7 @@ func ExampleClient_query_agg() {
|
||||
As: "num_affordable",
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
).Result()
|
||||
@@ -323,14 +317,12 @@ func ExampleClient_query_agg() {
|
||||
res3, err := rdb.FTAggregateWithArgs(ctx,
|
||||
"idx:bicycle", "*",
|
||||
&redis.FTAggregateOptions{
|
||||
Apply: []redis.FTAggregateApply{
|
||||
{
|
||||
Steps: []redis.FTAggregateStep{
|
||||
{Apply: &redis.FTAggregateApply{
|
||||
Field: "'bicycle'",
|
||||
As: "type",
|
||||
},
|
||||
},
|
||||
GroupBy: []redis.FTAggregateGroupBy{
|
||||
{
|
||||
}},
|
||||
{GroupBy: &redis.FTAggregateGroupBy{
|
||||
Fields: []interface{}{"@type"},
|
||||
Reduce: []redis.FTAggregateReducer{
|
||||
{
|
||||
@@ -338,7 +330,7 @@ func ExampleClient_query_agg() {
|
||||
As: "num_total",
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
).Result()
|
||||
@@ -363,11 +355,9 @@ func ExampleClient_query_agg() {
|
||||
res4, err := rdb.FTAggregateWithArgs(ctx,
|
||||
"idx:bicycle", "*",
|
||||
&redis.FTAggregateOptions{
|
||||
Load: []redis.FTAggregateLoad{
|
||||
{Field: "__key"},
|
||||
},
|
||||
GroupBy: []redis.FTAggregateGroupBy{
|
||||
{
|
||||
Steps: []redis.FTAggregateStep{
|
||||
{Load: &redis.FTAggregateLoad{Field: "__key"}},
|
||||
{GroupBy: &redis.FTAggregateGroupBy{
|
||||
Fields: []interface{}{"@condition"},
|
||||
Reduce: []redis.FTAggregateReducer{
|
||||
{
|
||||
@@ -376,7 +366,7 @@ func ExampleClient_query_agg() {
|
||||
As: "bicycles",
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
).Result()
|
||||
|
||||
@@ -257,6 +257,6 @@ func ExampleClient_search_qs() {
|
||||
|
||||
// Output:
|
||||
// Documents found: 10
|
||||
// {1 [{bicycle:0 <nil> <nil> <nil> map[$:{"brand":"Velorim","condition":"new","description":"Small and powerful, the Jigger is the best ride for the smallest of tikes! This is the tiniest kids’ pedal bike on the market available without a coaster brake, the Jigger is the vehicle of choice for the rare tenacious little rider raring to go.","model":"Jigger","price":270}] <nil>}]}
|
||||
// {1 [{bicycle:4 <nil> <nil> <nil> map[$:{"brand":"Noka Bikes","condition":"used","description":"Whether you want to try your hand at XC racing or are looking for a lively trail bike that's just as inspiring on the climbs as it is over rougher ground, the Wilder is one heck of a bike built specifically for short women. Both the frames and components have been tweaked to include a women’s saddle, different bars and unique colourway.","model":"Kahuna","price":3200}] <nil>}]}
|
||||
// {1 [{bicycle:0 <nil> <nil> <nil> map[$:{"brand":"Velorim","condition":"new","description":"Small and powerful, the Jigger is the best ride for the smallest of tikes! This is the tiniest kids’ pedal bike on the market available without a coaster brake, the Jigger is the vehicle of choice for the rare tenacious little rider raring to go.","model":"Jigger","price":270}] <nil>}] []}
|
||||
// {1 [{bicycle:4 <nil> <nil> <nil> map[$:{"brand":"Noka Bikes","condition":"used","description":"Whether you want to try your hand at XC racing or are looking for a lively trail bike that's just as inspiring on the climbs as it is over rougher ground, the Wilder is one heck of a bike built specifically for short women. Both the frames and components have been tweaked to include a women’s saddle, different bars and unique colourway.","model":"Kahuna","price":3200}] <nil>}] []}
|
||||
}
|
||||
|
||||
@@ -401,7 +401,7 @@ func ExampleClient_racefrance2() {
|
||||
}
|
||||
|
||||
fmt.Println(res13)
|
||||
// >>> [{1692632086370-0 map[location_id:1 position:1 rider:Castilla speed:30.2]}]
|
||||
// >>> [{1692632086370-0 map[location_id:1 position:1 rider:Castilla speed:30.2] 0 0}]
|
||||
// STEP_END
|
||||
|
||||
// STEP_START xrange_step_1
|
||||
@@ -412,7 +412,7 @@ func ExampleClient_racefrance2() {
|
||||
}
|
||||
|
||||
fmt.Println(res14)
|
||||
// >>> [{1692632086370-0 map[location_id:1 position:1 rider:Castilla speed:30.2]} {1692632094485-0 map[location_id:1 position:3 rider:Norem speed:28.8]}]
|
||||
// >>> [{1692632086370-0 map[location_id:1 position:1 rider:Castilla speed:30.2] 0 0} {1692632094485-0 map[location_id:1 position:3 rider:Norem speed:28.8] 0 0}]
|
||||
// STEP_END
|
||||
|
||||
// STEP_START xrange_step_2
|
||||
@@ -425,7 +425,7 @@ func ExampleClient_racefrance2() {
|
||||
}
|
||||
|
||||
fmt.Println(res15)
|
||||
// >>> [{1692632102976-0 map[location_id:1 position:2 rider:Prickett speed:29.7]} {1692632147973-0 map[location_id:2 position:1 rider:Castilla speed:29.9]}]
|
||||
// >>> [{1692632102976-0 map[location_id:1 position:2 rider:Prickett speed:29.7] 0 0} {1692632147973-0 map[location_id:2 position:1 rider:Castilla speed:29.9] 0 0}]
|
||||
// STEP_END
|
||||
|
||||
// STEP_START xrange_empty
|
||||
@@ -449,7 +449,7 @@ func ExampleClient_racefrance2() {
|
||||
}
|
||||
|
||||
fmt.Println(res17)
|
||||
// >>> [{1692632147973-0 map[location_id:2 position:1 rider:Castilla speed:29.9]}]
|
||||
// >>> [{1692632147973-0 map[location_id:2 position:1 rider:Castilla speed:29.9] 0 0}]
|
||||
// STEP_END
|
||||
|
||||
// STEP_START xread
|
||||
@@ -463,7 +463,7 @@ func ExampleClient_racefrance2() {
|
||||
}
|
||||
|
||||
fmt.Println(res18)
|
||||
// >>> [{race:france [{1692632086370-0 map[location_id:1 position:1 rider:Castilla speed:30.2]} {1692632094485-0 map[location_id:1 position:3 rider:Norem speed:28.8]}]}]
|
||||
// >>> [{race:france [{1692632086370-0 map[location_id:1 position:1 rider:Castilla speed:30.2] 0 0} {1692632094485-0 map[location_id:1 position:3 rider:Norem speed:28.8] 0 0}]}]
|
||||
// STEP_END
|
||||
|
||||
// Output:
|
||||
@@ -639,7 +639,7 @@ func ExampleClient_xgroupread() {
|
||||
}
|
||||
|
||||
// fmt.Println(res21)
|
||||
// >>> [{race:italy [{1692632639151-0 map[rider:Castilla]}]}]
|
||||
// >>> [{race:italy [{1692632639151-0 map[rider:Castilla] 0 0}]}]
|
||||
// STEP_END
|
||||
|
||||
// REMOVE_START
|
||||
@@ -754,7 +754,7 @@ func ExampleClient_raceitaly() {
|
||||
}
|
||||
|
||||
fmt.Println(res22)
|
||||
// >>> [{race:italy [{1692632639151-0 map[rider:Castilla]}]}]
|
||||
// >>> [{race:italy [{1692632639151-0 map[rider:Castilla] 0 0}]}]
|
||||
// STEP_END
|
||||
|
||||
// STEP_START xack
|
||||
@@ -795,7 +795,7 @@ func ExampleClient_raceitaly() {
|
||||
}
|
||||
|
||||
fmt.Println(res25)
|
||||
// >>> [{race:italy [{1692632647899-0 map[rider:Royce]} {1692632662819-0 map[rider:Sam-Bodden]}]}]
|
||||
// >>> [{race:italy [{1692632647899-0 map[rider:Royce] 0 0} {1692632662819-0 map[rider:Sam-Bodden] 0 0}]}]
|
||||
|
||||
// STEP_END
|
||||
|
||||
@@ -836,7 +836,7 @@ func ExampleClient_raceitaly() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Println(res28) // >>> [{1692632647899-0 map[rider:Royce]}]
|
||||
fmt.Println(res28) // >>> [{1692632647899-0 map[rider:Royce] 0 0}]
|
||||
// STEP_END
|
||||
|
||||
// STEP_START xclaim
|
||||
@@ -868,7 +868,7 @@ func ExampleClient_raceitaly() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Println(res30) // >>> [{1692632647899-0 map[rider:Royce]}]
|
||||
fmt.Println(res30) // >>> [{1692632647899-0 map[rider:Royce] 0 0}]
|
||||
fmt.Println(res30a) // >>> 1692632662819-0
|
||||
// STEP_END
|
||||
|
||||
@@ -889,36 +889,53 @@ func ExampleClient_raceitaly() {
|
||||
fmt.Println(res31a) // >>> 0-0
|
||||
// STEP_END
|
||||
|
||||
// STEP_START xinfo
|
||||
res32, err := rdb.XInfoStream(ctx, "race:italy").Result()
|
||||
// STEP_START xnack
|
||||
res32, err := rdb.XNack(ctx, &redis.XNackArgs{
|
||||
Stream: "race:italy",
|
||||
Group: "italy_riders",
|
||||
Mode: "FAIL",
|
||||
IDs: []string{"1692632662819-0"},
|
||||
}).Result()
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Println(res32)
|
||||
// >>> &{5 1 2 1 1692632678249-0 0-0 5 {1692632639151-0 map[rider:Castilla]} {1692632678249-0 map[rider:Norem]} 1692632639151-0}
|
||||
fmt.Println(res32) // >>> 1
|
||||
// STEP_END
|
||||
|
||||
// STEP_START xinfo
|
||||
res33, err := rdb.XInfoStream(ctx, "race:italy").Result()
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Println(res33.Length)
|
||||
// >>> 5
|
||||
fmt.Println(res33.FirstEntry)
|
||||
// >>> {1692632639151-0 map[rider:Castilla] 0 0}
|
||||
// STEP_END
|
||||
|
||||
// STEP_START xinfo_groups
|
||||
res33, err := rdb.XInfoGroups(ctx, "race:italy").Result()
|
||||
res34, err := rdb.XInfoGroups(ctx, "race:italy").Result()
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Println(res33)
|
||||
fmt.Println(res34)
|
||||
// >>> [{italy_riders 3 2 1692632662819-0 3 2}]
|
||||
// STEP_END
|
||||
|
||||
// STEP_START xinfo_consumers
|
||||
res34, err := rdb.XInfoConsumers(ctx, "race:italy", "italy_riders").Result()
|
||||
res35, err := rdb.XInfoConsumers(ctx, "race:italy", "italy_riders").Result()
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// fmt.Println(res34)
|
||||
// fmt.Println(res35)
|
||||
// >>> [{Alice 1 1ms 1ms} {Bob 1 2ms 2ms} {Lora 0 1ms -1ms}]
|
||||
// STEP_END
|
||||
|
||||
@@ -956,36 +973,26 @@ func ExampleClient_raceitaly() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
res35, err := rdb.XLen(ctx, "race:italy").Result()
|
||||
res36, err := rdb.XLen(ctx, "race:italy").Result()
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Println(res35) // >>> 2
|
||||
fmt.Println(res36) // >>> 2
|
||||
|
||||
res36, err := rdb.XRange(ctx, "race:italy", "-", "+").Result()
|
||||
res37, err := rdb.XRange(ctx, "race:italy", "-", "+").Result()
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// fmt.Println(res36)
|
||||
// >>> [{1726649529170-1 map[rider:Wood]} {1726649529171-0 map[rider:Henshaw]}]
|
||||
// fmt.Println(res37)
|
||||
// >>> [{1726649529170-1 map[rider:Wood] 0 0} {1726649529171-0 map[rider:Henshaw] 0 0}]
|
||||
// STEP_END
|
||||
|
||||
// STEP_START xtrim
|
||||
res37, err := rdb.XTrimMaxLen(ctx, "race:italy", 10).Result()
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Println(res37) // >>> 0
|
||||
// STEP_END
|
||||
|
||||
// STEP_START xtrim2
|
||||
res38, err := rdb.XTrimMaxLenApprox(ctx, "race:italy", 10, 20).Result()
|
||||
res38, err := rdb.XTrimMaxLen(ctx, "race:italy", 10).Result()
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -994,8 +1001,18 @@ func ExampleClient_raceitaly() {
|
||||
fmt.Println(res38) // >>> 0
|
||||
// STEP_END
|
||||
|
||||
// STEP_START xtrim2
|
||||
res39, err := rdb.XTrimMaxLenApprox(ctx, "race:italy", 10, 20).Result()
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Println(res39) // >>> 0
|
||||
// STEP_END
|
||||
|
||||
// REMOVE_START
|
||||
UNUSED(res27, res34, res36)
|
||||
UNUSED(res27, res35, res37)
|
||||
// REMOVE_END
|
||||
|
||||
// Output:
|
||||
@@ -1010,7 +1027,9 @@ func ExampleClient_raceitaly() {
|
||||
// 1692632662819-0
|
||||
// []
|
||||
// 0-0
|
||||
// &{5 1 2 1 1692632678249-0 0-0 5 {1692632639151-0 map[rider:Castilla] 0 0} {1692632678249-0 map[rider:Norem] 0 0} 1692632639151-0}
|
||||
// 1
|
||||
// 5
|
||||
// {1692632639151-0 map[rider:Castilla] 0 0}
|
||||
// [{italy_riders 3 2 1692632662819-0 3 2}]
|
||||
// 2
|
||||
// 0
|
||||
@@ -1064,7 +1083,7 @@ func ExampleClient_xdel() {
|
||||
}
|
||||
|
||||
fmt.Println(res39)
|
||||
// >>> [{1692633198206-0 map[rider:Wood]} {1692633208557-0 map[rider:Henshaw]}]
|
||||
// >>> [{1692633198206-0 map[rider:Wood] 0 0} {1692633208557-0 map[rider:Henshaw] 0 0}]
|
||||
|
||||
res40, err := rdb.XDel(ctx, "race:italy", "1692633208557-0").Result()
|
||||
|
||||
@@ -1081,7 +1100,7 @@ func ExampleClient_xdel() {
|
||||
}
|
||||
|
||||
fmt.Println(res41)
|
||||
// >>> [{1692633198206-0 map[rider:Wood]}]
|
||||
// >>> [{1692633198206-0 map[rider:Wood] 0 0}]
|
||||
// STEP_END
|
||||
|
||||
// Output:
|
||||
|
||||
@@ -5,7 +5,9 @@ package example_commands_test
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"maps"
|
||||
"math"
|
||||
"slices"
|
||||
"sort"
|
||||
|
||||
"github.com/hanzoai/kv-go/v9"
|
||||
@@ -13,16 +15,6 @@ import (
|
||||
|
||||
// HIDE_END
|
||||
|
||||
// mapKeys returns a slice of all keys from the map (Go 1.21 compatible)
|
||||
// TODO: Once minimum Go version is upgraded to 1.23+, replace with slices.Collect(maps.Keys(m))
|
||||
func mapKeys[K comparable, V any](m map[K]V) []K {
|
||||
keys := make([]K, 0, len(m))
|
||||
for k := range m {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
func ExampleClient_timeseries_create() {
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -425,7 +417,7 @@ func ExampleClient_timeseries_query_multi() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
res28Keys := mapKeys(res28)
|
||||
res28Keys := slices.Collect(maps.Keys(res28))
|
||||
sort.Strings(res28Keys)
|
||||
|
||||
for _, k := range res28Keys {
|
||||
@@ -465,7 +457,7 @@ func ExampleClient_timeseries_query_multi() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
res29Keys := mapKeys(res29)
|
||||
res29Keys := slices.Collect(maps.Keys(res29))
|
||||
sort.Strings(res29Keys)
|
||||
|
||||
for _, k := range res29Keys {
|
||||
@@ -513,7 +505,7 @@ func ExampleClient_timeseries_query_multi() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
res30Keys := mapKeys(res30)
|
||||
res30Keys := slices.Collect(maps.Keys(res30))
|
||||
sort.Strings(res30Keys)
|
||||
|
||||
for _, k := range res30Keys {
|
||||
@@ -558,7 +550,7 @@ func ExampleClient_timeseries_query_multi() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
res31Keys := mapKeys(res31)
|
||||
res31Keys := slices.Collect(maps.Keys(res31))
|
||||
sort.Strings(res31Keys)
|
||||
|
||||
for _, k := range res31Keys {
|
||||
@@ -677,8 +669,28 @@ func ExampleClient_timeseries_aggregation() {
|
||||
// >>> [{0 1.9500000000000002} {2 2.0999999999999996} {4 1.78}]
|
||||
// STEP_END
|
||||
|
||||
// STEP_START agg_multi
|
||||
res33, err := rdb.TSRangeWithArgs(
|
||||
ctx,
|
||||
"rg:2",
|
||||
0,
|
||||
math.MaxInt64,
|
||||
&redis.TSRangeOptions{
|
||||
Aggregators: []redis.Aggregator{redis.Min, redis.Max},
|
||||
BucketDuration: 2,
|
||||
},
|
||||
).Result()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Println(res33)
|
||||
// >>> [{0 [1.8 2.1]} {2 [1.9 2.3]} {4 [1.78 1.78]}]
|
||||
// STEP_END
|
||||
|
||||
// Output:
|
||||
// [{0 1.9500000000000002} {2 2.0999999999999996} {4 1.78}]
|
||||
// [{0 [1.8 2.1]} {2 [1.9 2.3]} {4 [1.78 1.78]}]
|
||||
}
|
||||
func ExampleClient_timeseries_agg_bucket() {
|
||||
ctx := context.Background()
|
||||
@@ -865,7 +877,7 @@ func ExampleClient_timeseries_aggmulti() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
res44Keys := mapKeys(res44)
|
||||
res44Keys := slices.Collect(maps.Keys(res44))
|
||||
sort.Strings(res44Keys)
|
||||
|
||||
for _, k := range res44Keys {
|
||||
@@ -913,7 +925,7 @@ func ExampleClient_timeseries_aggmulti() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
res45Keys := mapKeys(res45)
|
||||
res45Keys := slices.Collect(maps.Keys(res45))
|
||||
sort.Strings(res45Keys)
|
||||
|
||||
for _, k := range res45Keys {
|
||||
|
||||
@@ -345,6 +345,62 @@ func ExampleClient_vectorset() {
|
||||
}
|
||||
|
||||
fmt.Println(res30) // >>> [pt:C pt:B]
|
||||
|
||||
res31, err := rdb.VIsMember(ctx, "points", "pt:A").Result()
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Println(res31) // >>> true
|
||||
|
||||
res32, err := rdb.VIsMember(ctx, "points", "pt:W").Result()
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Println(res32) // >>> false
|
||||
|
||||
res33, err := rdb.VSimWithArgsWithAttribs(ctx, "points",
|
||||
&redis.VectorRef{Name: "pt:A"},
|
||||
&redis.VSimArgs{Count: 1, Truth: true},
|
||||
).Result()
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Println(res33[0].Name, *res33[0].Attribs) // >>> pt:A {"price":18.99,"size":"large"}
|
||||
|
||||
res34, err := rdb.VSimWithArgsWithScoresWithAttribs(ctx, "points",
|
||||
&redis.VectorRef{Name: "pt:A"},
|
||||
&redis.VSimArgs{Count: 1, Truth: true},
|
||||
).Result()
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Println(res34[0].Name, res34[0].Score, *res34[0].Attribs) // >>> pt:A 1 {"price":18.99,"size":"large"}
|
||||
// STEP_END
|
||||
|
||||
// STEP_START vlinks
|
||||
res35, err := rdb.VLinks(ctx, "points", "pt:A").Result()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Print only the last layer (deterministic)
|
||||
fmt.Println(res35[len(res35)-1]) // [pt:B pt:C pt:D pt:E]
|
||||
|
||||
res36, err := rdb.VLinksWithScores(ctx, "points", "pt:A").Result()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Print only the last layer (deterministic)
|
||||
fmt.Println(res36[len(res36)-1]) // [{pt:B 0} {pt:C 0.5} {pt:D 0.5} {pt:E 0.8535534143447876}]
|
||||
// STEP_END
|
||||
|
||||
// Output:
|
||||
@@ -378,6 +434,12 @@ func ExampleClient_vectorset() {
|
||||
// true
|
||||
// [pt:A pt:C pt:B]
|
||||
// [pt:C pt:B]
|
||||
// true
|
||||
// false
|
||||
// pt:A {"price":18.99,"size":"large"}
|
||||
// pt:A 1 {"price":18.99,"size":"large"}
|
||||
// [pt:B pt:C pt:D pt:E]
|
||||
// [{pt:B 0} {pt:C 0.5} {pt:D 0.5} {pt:E 0.8535534143447876}]
|
||||
}
|
||||
|
||||
func ExampleClient_vectorset_quantization() {
|
||||
|
||||
@@ -28,6 +28,11 @@ var ErrPoolTimeout = pool.ErrPoolTimeout
|
||||
// is used on a ClusterClient with keys in different slots.
|
||||
var ErrCrossSlot = proto.RedisError("CROSSSLOT Keys in request don't hash to the same slot")
|
||||
|
||||
// ErrNoScript is returned when EVALSHA is requested for a script digest that
|
||||
// is not available in the script cache. Note that this error text is reproduced
|
||||
// literally from that used by Redis.
|
||||
var ErrNoScript = proto.RedisError("NOSCRIPT No matching script. Please use EVAL.")
|
||||
|
||||
// HasErrorPrefix checks if the err is a Redis error and the message contains a prefix.
|
||||
func HasErrorPrefix(err error, prefix string) bool {
|
||||
var rErr Error
|
||||
@@ -86,6 +91,14 @@ func shouldRetry(err error, retryTimeout bool) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// Dial errors mean TCP connection was never established — safe to retry even
|
||||
// when wrapped inside context.DeadlineExceeded (from DialTimeout context).
|
||||
// Must be checked before the context error check below.
|
||||
var opErr *net.OpError
|
||||
if errors.As(err, &opErr) && opErr.Op == "dial" {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check for context errors (works with wrapped errors)
|
||||
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
||||
return false
|
||||
@@ -139,6 +152,9 @@ func shouldRetry(err error, retryTimeout bool) bool {
|
||||
if strings.HasPrefix(s, "READONLY ") {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(s, "-READONLY You can't write against a read only replica") {
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(s, "CLUSTERDOWN ") {
|
||||
return true
|
||||
}
|
||||
|
||||
+10
-9
@@ -3,6 +3,7 @@ package redis_test
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
|
||||
. "github.com/bsm/ginkgo/v2"
|
||||
. "github.com/bsm/gomega"
|
||||
@@ -24,11 +25,9 @@ func (t testTimeout) Error() string {
|
||||
|
||||
var _ = Describe("error", func() {
|
||||
BeforeEach(func() {
|
||||
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
|
||||
})
|
||||
|
||||
It("should retry", func() {
|
||||
@@ -39,14 +38,16 @@ var _ = Describe("error", func() {
|
||||
context.Canceled: false,
|
||||
context.DeadlineExceeded: false,
|
||||
redis.ErrPoolTimeout: true,
|
||||
&net.OpError{Op: "dial"}: true,
|
||||
// Use typed errors instead of plain errors.New()
|
||||
proto.ParseErrorReply([]byte("-ERR max number of clients reached")): true,
|
||||
proto.ParseErrorReply([]byte("-LOADING Redis is loading the dataset in memory")): true,
|
||||
proto.ParseErrorReply([]byte("-READONLY You can't write against a read only replica")): true,
|
||||
proto.ParseErrorReply([]byte("-CLUSTERDOWN The cluster is down")): true,
|
||||
proto.ParseErrorReply([]byte("-TRYAGAIN Command cannot be processed, please try again")): true,
|
||||
proto.ParseErrorReply([]byte("-NOREPLICAS Not enough good replicas to write")): true,
|
||||
proto.ParseErrorReply([]byte("-ERR other")): false,
|
||||
proto.ParseErrorReply([]byte("-ERR max number of clients reached")): true,
|
||||
proto.ParseErrorReply([]byte("-LOADING Redis is loading the dataset in memory")): true,
|
||||
proto.ParseErrorReply([]byte("-READONLY You can't write against a read only replica")): true,
|
||||
proto.ParseErrorReply([]byte("-ERR Error running script (call to f_abc123): @user_script:1: -READONLY You can't write against a read only replica.")): true,
|
||||
proto.ParseErrorReply([]byte("-CLUSTERDOWN The cluster is down")): true,
|
||||
proto.ParseErrorReply([]byte("-TRYAGAIN Command cannot be processed, please try again")): true,
|
||||
proto.ParseErrorReply([]byte("-NOREPLICAS Not enough good replicas to write")): true,
|
||||
proto.ParseErrorReply([]byte("-ERR other")): false,
|
||||
}
|
||||
|
||||
for err, expected := range data {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -256,6 +257,12 @@ func TestShouldRetryWithTypedErrors(t *testing.T) {
|
||||
shouldRetry: true,
|
||||
retryTimeout: false,
|
||||
},
|
||||
{
|
||||
name: "Lua script READONLY error should retry",
|
||||
errorMsg: "ERR Error running script (call to f_abc123): @user_script:1: -READONLY You can't write against a read only replica.",
|
||||
shouldRetry: true,
|
||||
retryTimeout: false,
|
||||
},
|
||||
{
|
||||
name: "CLUSTERDOWN error should retry",
|
||||
errorMsg: "CLUSTERDOWN The cluster is down",
|
||||
@@ -304,6 +311,51 @@ func TestShouldRetryWithTypedErrors(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestLuaScriptReadOnlyError tests that READONLY errors from Lua scripts
|
||||
// are correctly detected by IsReadOnlyError and ShouldRetry.
|
||||
func TestLuaScriptReadOnlyError(t *testing.T) {
|
||||
luaReadOnlyMsg := "ERR Error running script (call to f_abc123): @user_script:1: -READONLY You can't write against a read only replica."
|
||||
|
||||
t.Run("IsReadOnlyError detects Lua script READONLY", func(t *testing.T) {
|
||||
err := proto.ParseErrorReply([]byte("-" + luaReadOnlyMsg))
|
||||
if !redis.IsReadOnlyError(err) {
|
||||
t.Errorf("IsReadOnlyError should detect Lua script READONLY error: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ShouldRetry retries Lua script READONLY", func(t *testing.T) {
|
||||
err := proto.ParseErrorReply([]byte("-" + luaReadOnlyMsg))
|
||||
if !redis.ShouldRetry(err, false) {
|
||||
t.Errorf("ShouldRetry should retry Lua script READONLY error: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("wrapped Lua script READONLY detected", func(t *testing.T) {
|
||||
err := proto.ParseErrorReply([]byte("-" + luaReadOnlyMsg))
|
||||
wrappedErr := fmt.Errorf("hook wrapper: %w", err)
|
||||
if !redis.IsReadOnlyError(wrappedErr) {
|
||||
t.Errorf("IsReadOnlyError should detect wrapped Lua script READONLY error: %v", wrappedErr)
|
||||
}
|
||||
if !redis.ShouldRetry(wrappedErr, false) {
|
||||
t.Errorf("ShouldRetry should retry wrapped Lua script READONLY error: %v", wrappedErr)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("false positive: non-script error with -READONLY not matched", func(t *testing.T) {
|
||||
err := proto.ParseErrorReply([]byte("-ERR something-READONLY-key"))
|
||||
if redis.IsReadOnlyError(err) {
|
||||
t.Errorf("IsReadOnlyError should NOT match non-script error containing -READONLY: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("false positive: script error without -READONLY not matched", func(t *testing.T) {
|
||||
err := proto.ParseErrorReply([]byte("-ERR Error running script (call to f_abc): @user_script:1: some other error"))
|
||||
if redis.IsReadOnlyError(err) {
|
||||
t.Errorf("IsReadOnlyError should NOT match script error without -READONLY: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestSetErrWithWrappedError tests that when a hook wraps an error and sets it
|
||||
// via cmd.SetErr(), the underlying typed error can still be detected
|
||||
func TestSetErrWithWrappedError(t *testing.T) {
|
||||
@@ -587,6 +639,59 @@ func TestContextErrorWrapping(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// TestDialTimeoutShouldRetry tests that dial errors wrapped in context.DeadlineExceeded
|
||||
// (as produced by pool.go's context.WithTimeout(ctx, DialTimeout)) are retried.
|
||||
func TestDialTimeoutShouldRetry(t *testing.T) {
|
||||
makeDialOpError := func(inner error) *net.OpError {
|
||||
return &net.OpError{Op: "dial", Net: "tcp", Err: inner}
|
||||
}
|
||||
|
||||
t.Run("bare dial OpError retries", func(t *testing.T) {
|
||||
err := makeDialOpError(&net.AddrError{Err: "connection refused", Addr: "127.0.0.1:6379"})
|
||||
if !redis.ShouldRetry(err, false) {
|
||||
t.Errorf("bare dial OpError should retry")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("dial OpError wrapped in context.DeadlineExceeded retries", func(t *testing.T) {
|
||||
// Simulates pool.go wraps net.OpError as DeadlineExceeded.
|
||||
dialErr := makeDialOpError(context.DeadlineExceeded)
|
||||
wrappedErr := fmt.Errorf("dial tcp: %w", dialErr)
|
||||
if !redis.ShouldRetry(wrappedErr, false) {
|
||||
t.Errorf("dial OpError wrapped in DeadlineExceeded should retry, got false")
|
||||
}
|
||||
if !redis.ShouldRetry(wrappedErr, true) {
|
||||
t.Errorf("dial OpError wrapped in DeadlineExceeded should retry, got false")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("plain context.DeadlineExceeded does not retry", func(t *testing.T) {
|
||||
err := fmt.Errorf("op failed: %w", context.DeadlineExceeded)
|
||||
if redis.ShouldRetry(err, false) {
|
||||
t.Errorf("plain DeadlineExceeded should not retry")
|
||||
}
|
||||
if redis.ShouldRetry(err, true) {
|
||||
t.Errorf("plain DeadlineExceeded should not retry even with retryTimeout=true")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("plain context.Canceled does not retry", func(t *testing.T) {
|
||||
err := fmt.Errorf("op failed: %w", context.Canceled)
|
||||
if redis.ShouldRetry(err, false) {
|
||||
t.Errorf("plain context.Canceled should not retry")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("non-dial OpError does not bypass DeadlineExceeded check", func(t *testing.T) {
|
||||
// Op="read" wrapped in DeadlineExceeded should NOT retry (user context expired)
|
||||
readErr := &net.OpError{Op: "read", Net: "tcp", Err: context.DeadlineExceeded}
|
||||
wrappedErr := fmt.Errorf("read: %w", readErr)
|
||||
if redis.ShouldRetry(wrappedErr, false) {
|
||||
t.Errorf("non-dial OpError with DeadlineExceeded should not retry")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestIOErrorWrapping tests that io errors work correctly when wrapped
|
||||
func TestIOErrorWrapping(t *testing.T) {
|
||||
t.Run("Wrapped io.EOF", func(t *testing.T) {
|
||||
|
||||
@@ -8,5 +8,5 @@ require github.com/hanzoai/kv-go/v9 v9.16.0
|
||||
|
||||
require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
)
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
||||
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
|
||||
@@ -11,7 +11,6 @@ require (
|
||||
|
||||
require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
)
|
||||
|
||||
@@ -1,19 +1,32 @@
|
||||
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
|
||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
||||
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
|
||||
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
||||
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
||||
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
||||
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
|
||||
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -8,8 +8,8 @@ require github.com/hanzoai/kv-go/v9 v9.18.0-beta.2
|
||||
|
||||
require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
|
||||
github.com/zeebo/xxh3 v1.0.2 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||
github.com/zeebo/xxh3 v1.1.0 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
)
|
||||
|
||||
@@ -6,19 +6,17 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/dgryski/go-xxh3 v0.0.0-20190317160639-1675ae87ac49 h1:E2VBGIi+PvI5Nl6ddS5W0RVtoJRfpRVaunUC8Q/NOJI=
|
||||
github.com/dgryski/go-xxh3 v0.0.0-20190317160639-1675ae87ac49/go.mod h1:GEXsXVlzo0o2VPFcPwTUM6ssCHA4xn4fp8sdopBufVU=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
|
||||
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
|
||||
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
|
||||
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
||||
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
|
||||
@@ -8,6 +8,5 @@ require github.com/hanzoai/kv-go/v9 v9.7.0
|
||||
|
||||
require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
)
|
||||
|
||||
@@ -6,11 +6,15 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
||||
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
|
||||
@@ -8,6 +8,5 @@ require github.com/hanzoai/kv-go/v9 v9.18.0-beta.2
|
||||
|
||||
require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
)
|
||||
|
||||
+11
-2
@@ -1,11 +1,20 @@
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
||||
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
|
||||
@@ -11,6 +11,5 @@ require (
|
||||
|
||||
require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
)
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
||||
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
|
||||
@@ -8,6 +8,5 @@ require github.com/hanzoai/kv-go/v9 v9.18.0-beta.2
|
||||
|
||||
require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
)
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
||||
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
|
||||
@@ -8,6 +8,5 @@ require github.com/hanzoai/kv-go/v9 v9.11.0
|
||||
|
||||
require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
)
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
||||
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# OpenTelemetry Metrics Example
|
||||
|
||||
This example demonstrates how to enable OpenTelemetry metrics for Redis operations using the `extra/redisotel-native` package.
|
||||
|
||||
## Features
|
||||
|
||||
- ✅ OTLP exporter configuration
|
||||
- ✅ Periodic metric export (every 10 seconds)
|
||||
- ✅ Concurrent Redis operations
|
||||
- ✅ Automatic metric collection for:
|
||||
- Operation duration
|
||||
- Connection metrics
|
||||
- Error tracking
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Go 1.24.0 or later
|
||||
- Redis server running on `localhost:6379`
|
||||
- OTLP collector running on `localhost:4317` (optional)
|
||||
|
||||
## Running the Example
|
||||
|
||||
```bash
|
||||
# Start Redis (if not already running)
|
||||
redis-server
|
||||
|
||||
# Optional: Start OTLP collector
|
||||
# See: https://opentelemetry.io/docs/collector/
|
||||
|
||||
# Run the example
|
||||
go run main.go
|
||||
```
|
||||
|
||||
## What It Does
|
||||
|
||||
1. Creates an OTLP exporter that sends metrics to a collector
|
||||
2. Sets up a meter provider with periodic export (every 10 seconds)
|
||||
3. Initializes Redis client with OTel instrumentation
|
||||
4. Executes concurrent Redis operations (SET commands)
|
||||
5. Waits for metrics to be exported
|
||||
|
||||
## Metrics Collected
|
||||
|
||||
The example automatically collects:
|
||||
|
||||
- **db.client.operation.duration** - Operation latency histogram
|
||||
- **db.client.connection.create_time** - Connection creation time
|
||||
- **db.client.connection.count** - Active connection count
|
||||
- **db.client.errors** - Error counter with error type classification
|
||||
|
||||
## Configuration
|
||||
|
||||
To use with a production OTLP collector:
|
||||
|
||||
```go
|
||||
exporter, err := otlpmetricgrpc.New(ctx,
|
||||
otlpmetricgrpc.WithEndpoint("your-collector:4317"),
|
||||
otlpmetricgrpc.WithTLSCredentials(credentials.NewClientTLSFromCert(certPool, "")),
|
||||
)
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [OpenTelemetry Go SDK](https://opentelemetry.io/docs/languages/go/)
|
||||
- [OTLP Exporter Documentation](https://opentelemetry.io/docs/specs/otlp/)
|
||||
- [Redis OTel Native Package](../../extra/redisotel-native/)
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
module github.com/redis/go-redis/example/otel-metrics
|
||||
|
||||
go 1.25.0
|
||||
|
||||
replace github.com/redis/go-redis/v9 => ../..
|
||||
|
||||
replace github.com/redis/go-redis/extra/redisotel-native/v9 => ../../extra/redisotel-native
|
||||
|
||||
require (
|
||||
github.com/redis/go-redis/extra/redisotel-native/v9 v9.20.0
|
||||
github.com/redis/go-redis/v9 v9.20.0
|
||||
go.opentelemetry.io/otel v1.43.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
golang.org/x/net v0.52.0 // indirect
|
||||
golang.org/x/sys v0.43.0 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d // indirect
|
||||
google.golang.org/grpc v1.80.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
)
|
||||
@@ -0,0 +1,67 @@
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
||||
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 h1:8UQVDcZxOJLtX6gxtDt3vY2WTgvZqMQRzjsqiIHQdkc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0/go.mod h1:2lmweYCiHYpEjQ/lSJBYhj9jP1zvCvQW4BqL9dnT7FQ=
|
||||
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
||||
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d h1:/aDRtSZJjyLQzm75d+a1wOJaqyKBMvIAfeQmoa3ORiI=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:etfGUgejTiadZAUaEP14NP97xi1RGeawqkjDARA/UOs=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d h1:wT2n40TBqFY6wiwazVK9/iTWbsQrgk5ZfCSVFLO9LQA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
|
||||
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -0,0 +1,123 @@
|
||||
// EXAMPLE: otel_metrics
|
||||
// HIDE_START
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
redisotel "github.com/redis/go-redis/extra/redisotel-native/v9"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
|
||||
"go.opentelemetry.io/otel/sdk/metric"
|
||||
)
|
||||
|
||||
// ExampleClient_otel_metrics demonstrates how to enable OpenTelemetry metrics
|
||||
// for Redis operations and export them to an OTLP collector.
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
|
||||
// HIDE_END
|
||||
|
||||
// STEP_START otel_exporter_setup
|
||||
// Create OTLP exporter that sends metrics to the collector
|
||||
// Default endpoint is localhost:4317 (gRPC)
|
||||
exporter, err := otlpmetricgrpc.New(ctx,
|
||||
otlpmetricgrpc.WithInsecure(), // Use insecure for local development
|
||||
// For production, configure TLS and authentication:
|
||||
// otlpmetricgrpc.WithEndpoint("your-collector:4317"),
|
||||
// otlpmetricgrpc.WithTLSCredentials(...),
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create OTLP exporter: %v", err)
|
||||
}
|
||||
// STEP_END
|
||||
|
||||
// STEP_START otel_meter_provider
|
||||
// Create meter provider with periodic reader
|
||||
// Metrics are exported every 10 seconds
|
||||
meterProvider := metric.NewMeterProvider(
|
||||
metric.WithReader(
|
||||
metric.NewPeriodicReader(exporter,
|
||||
metric.WithInterval(10*time.Second),
|
||||
),
|
||||
),
|
||||
)
|
||||
defer func() {
|
||||
if err := meterProvider.Shutdown(ctx); err != nil {
|
||||
log.Printf("Error shutting down meter provider: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Set the global meter provider
|
||||
otel.SetMeterProvider(meterProvider)
|
||||
// STEP_END
|
||||
|
||||
// STEP_START redis_client_setup
|
||||
// Initialize OTel instrumentation BEFORE creating Redis clients
|
||||
otelInstance := redisotel.GetObservabilityInstance()
|
||||
config := redisotel.NewConfig().WithEnabled(true)
|
||||
if err := otelInstance.Init(config); err != nil {
|
||||
log.Fatalf("Failed to initialize OTel: %v", err)
|
||||
}
|
||||
defer otelInstance.Shutdown()
|
||||
|
||||
// Create Redis client - automatically instrumented
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6379",
|
||||
})
|
||||
defer rdb.Close()
|
||||
// STEP_END
|
||||
|
||||
// STEP_START redis_operations
|
||||
// Execute Redis operations - metrics are automatically collected
|
||||
log.Println("Executing Redis operations...")
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(50)
|
||||
for i := range 50 {
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
|
||||
for j := range 10 {
|
||||
if err := rdb.Set(ctx, "key"+strconv.Itoa(i*10+j), "value", 0).Err(); err != nil {
|
||||
log.Printf("Error setting key: %v", err)
|
||||
}
|
||||
time.Sleep(time.Millisecond * time.Duration(rand.Intn(400)))
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
wg.Add(10)
|
||||
for i := range 10 {
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
|
||||
for j := range 10 {
|
||||
if err := rdb.Set(ctx, "key"+strconv.Itoa(i*10+j), "value", 0).Err(); err != nil {
|
||||
log.Printf("Error setting key: %v", err)
|
||||
}
|
||||
time.Sleep(time.Millisecond * time.Duration(rand.Intn(400)))
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
for j := range 10 {
|
||||
if err := rdb.Set(ctx, "key"+strconv.Itoa(j), "value", 0).Err(); err != nil {
|
||||
log.Printf("Error setting key: %v", err)
|
||||
}
|
||||
time.Sleep(time.Millisecond * time.Duration(rand.Intn(400)))
|
||||
}
|
||||
|
||||
log.Println("Operations complete. Waiting for metrics to be exported...")
|
||||
|
||||
// Wait for metrics to be exported
|
||||
time.Sleep(15 * time.Second)
|
||||
// STEP_END
|
||||
}
|
||||
+9
-11
@@ -18,10 +18,9 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/go-logr/logr v1.4.1 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect
|
||||
@@ -37,12 +36,11 @@ require (
|
||||
go.opentelemetry.io/otel/trace v1.22.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.0.0 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
golang.org/x/net v0.36.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20240108191215-35c7eff3a6b1 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240108191215-35c7eff3a6b1 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1 // indirect
|
||||
google.golang.org/grpc v1.60.1 // indirect
|
||||
google.golang.org/protobuf v1.33.0 // indirect
|
||||
golang.org/x/net v0.52.0 // indirect
|
||||
golang.org/x/sys v0.43.0 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d // indirect
|
||||
google.golang.org/grpc v1.80.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
)
|
||||
|
||||
+70
-59
@@ -2,77 +2,88 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
|
||||
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.5.5/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/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/uptrace/uptrace-go v1.21.0 h1:oJoUjhiVT7aiuoG6B3ClVHtJozLn3cK9hQt8U5dQO1M=
|
||||
github.com/uptrace/uptrace-go v1.21.0/go.mod h1:/aXAFGKOqeAFBqWa1xtzLnGX2xJm1GScqz9NJ0TJjLM=
|
||||
go.opentelemetry.io/contrib/instrumentation/runtime v0.46.1 h1:m9ReioVPIffxjJlGNRd0d5poy+9oTro3D+YbiEzUDOc=
|
||||
go.opentelemetry.io/contrib/instrumentation/runtime v0.46.1/go.mod h1:CANkrsXNzqOKXfOomu2zhOmc1/J5UZK9SGjrat6ZCG0=
|
||||
go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y=
|
||||
go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 h1:jd0+5t/YynESZqsSyPz+7PAFdEop0dlN0+PkyHYo8oI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0/go.mod h1:U707O40ee1FpQGyhvqnzmCJm1Wh6OX6GGBVn0E6Uyyk=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 h1:cl5P5/GIfFh4t6xyruOgJP5QiA1pw4fYYdv6nc6CBWw=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0/go.mod h1:zgBdWWAu7oEEMC06MMKc5NLbA/1YDXV1sMpSqEeLQLg=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 h1:tIqheXEFWAZ7O8A7m+J0aPTmpJN3YQ7qetUAdkkkKpk=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0/go.mod h1:nUeKExfxAQVbiVFn32YXpXZZHZ61Cc3s3Rn1pDBGAb0=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.21.0 h1:VhlEQAPp9R1ktYfrPk5SOryw1e9LDDTZCbIPFrho0ec=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.21.0/go.mod h1:kB3ufRbfU+CQ4MlUcqtW8Z7YEOBeK2DJ6CmR5rYYF3E=
|
||||
go.opentelemetry.io/otel/metric v1.22.0 h1:lypMQnGyJYeuYPhOM/bgjbFM6WE44W1/T45er4d8Hhg=
|
||||
go.opentelemetry.io/otel/metric v1.22.0/go.mod h1:evJGjVpZv0mQ5QBRJoBF64yMuOf4xCWdXjK8pzFvliY=
|
||||
go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw=
|
||||
go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.21.0 h1:smhI5oD714d6jHE6Tie36fPx4WDFIg+Y6RfAY4ICcR0=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.21.0/go.mod h1:FJ8RAsoPGv/wYMgBdUJXOm+6pzFY3YdljnXtv1SBE8Q=
|
||||
go.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0=
|
||||
go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo=
|
||||
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
|
||||
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/uptrace/uptrace-go v1.41.1 h1:EtWkkdOQqtuJMZyzeU0zT5VH6ppVY12yOouQK3VRccw=
|
||||
github.com/uptrace/uptrace-go v1.41.1/go.mod h1:gdn1eRLG3KCtTyiw+L8tG+tb/wnpiyIfLfTH2qh/5Mw=
|
||||
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
||||
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/runtime v0.68.0 h1:jhVIQEprwUTV+KfzzliLidclhoTOoHTgdz96kAyR8mU=
|
||||
go.opentelemetry.io/contrib/instrumentation/runtime v0.68.0/go.mod h1:4HsdbLUbernaTnA8CNaNE+1g026SciXb3juRYe3l8EY=
|
||||
go.opentelemetry.io/contrib/processors/minsev v0.16.0 h1:bjTZkvAKnG1mqWgCjU7RkOkHRTMsGlJO/UlqjRCweeU=
|
||||
go.opentelemetry.io/contrib/processors/minsev v0.16.0/go.mod h1:R2mmaDsqsWb+Y0mQkPifiCwifdotrG4fFoD4z0tim+g=
|
||||
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 h1:HIBTQ3VO5aupLKjC90JgMqpezVXwFuq6Ryjn0/izoag=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0/go.mod h1:ji9vId85hMxqfvICA0Jt8JqEdrXaAkcpkI9HPXya0ro=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 h1:w1K+pCJoPpQifuVpsKamUdn9U0zM3xUziVOqsGksUrY=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0/go.mod h1:HBy4BjzgVE8139ieRI75oXm3EcDN+6GhD88JT1Kjvxg=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0 h1:mS47AX77OtFfKG4vtp+84kuGSFZHTyxtXIN269vChY0=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0/go.mod h1:PJnsC41lAGncJlPUniSwM81gc80GkgWJWr3cu2nKEtU=
|
||||
go.opentelemetry.io/otel/log v0.19.0 h1:KUZs/GOsw79TBBMfDWsXS+KZ4g2Ckzksd1ymzsIEbo4=
|
||||
go.opentelemetry.io/otel/log v0.19.0/go.mod h1:5DQYeGmxVIr4n0/BcJvF4upsraHjg6vudJJpnkL6Ipk=
|
||||
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
||||
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
||||
go.opentelemetry.io/otel/sdk/log v0.19.0 h1:scYVLqT22D2gqXItnWiocLUKGH9yvkkeql5dBDiXyko=
|
||||
go.opentelemetry.io/otel/sdk/log v0.19.0/go.mod h1:vFBowwXGLlW9AvpuF7bMgnNI95LiW10szrOdvzBHlAg=
|
||||
go.opentelemetry.io/otel/sdk/log/logtest v0.19.0 h1:BEbF7ZBB6qQloV/Ub1+3NQoOUnVtcGkU3XX4Ws3GQfk=
|
||||
go.opentelemetry.io/otel/sdk/log/logtest v0.19.0/go.mod h1:Lua81/3yM0wOmoHTokLj9y9ADeA02v1naRrVrkAZuKk=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA=
|
||||
golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto v0.0.0-20240108191215-35c7eff3a6b1 h1:/IWabOtPziuXTEtI1KYCpM6Ss7vaAkeMxk+uXV/xvZs=
|
||||
google.golang.org/genproto v0.0.0-20240108191215-35c7eff3a6b1/go.mod h1:+Rvu7ElI+aLzyDQhpHMFMMltsD6m7nqpuWDd2CwJw3k=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240108191215-35c7eff3a6b1 h1:OPXtXn7fNMaXwO3JvOmF1QyTc00jsSFFz1vXXBOdCDo=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240108191215-35c7eff3a6b1/go.mod h1:B5xPO//w8qmBDjGReYLpR6UJPnkldGkCSMoH/2vxJeg=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1 h1:gphdwh0npgs8elJ4T6J+DQJHPVF7RsuJHCfwztUb4J4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1/go.mod h1:daQN87bsDqDoe316QbbvX60nMoJQa4r6Ds0ZuoAe5yA=
|
||||
google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU=
|
||||
google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d h1:/aDRtSZJjyLQzm75d+a1wOJaqyKBMvIAfeQmoa3ORiI=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:etfGUgejTiadZAUaEP14NP97xi1RGeawqkjDARA/UOs=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d h1:wT2n40TBqFY6wiwazVK9/iTWbsQrgk5ZfCSVFLO9LQA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
|
||||
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -8,6 +8,5 @@ require github.com/hanzoai/kv-go/v9 v9.18.0-beta.2
|
||||
|
||||
require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
)
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
||||
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
|
||||
@@ -11,6 +11,5 @@ require (
|
||||
|
||||
require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
)
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
||||
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
# FT.AGGREGATE step-based pipeline example
|
||||
|
||||
This example demonstrates `FTAggregateOptions.Steps`, which lets `LOAD`,
|
||||
`APPLY`, `GROUPBY` and `SORTBY` appear multiple times in any order in an
|
||||
`FT.AGGREGATE` pipeline.
|
||||
|
||||
The main motivation is shard-level trimming: running a `SORTBY ... MAX N`
|
||||
**before** a `GROUPBY` so each shard only ships its top-N rows onward.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
A Redis instance with the Search module loaded (Redis 8+, or Redis Stack)
|
||||
listening on `localhost:6379`.
|
||||
|
||||
The client is configured with `Protocol: 2` because the RESP3 response shape
|
||||
of `FT.AGGREGATE` is still marked unstable; set `UnstableResp3: true` instead
|
||||
if you want to use RESP3.
|
||||
|
||||
## Run
|
||||
|
||||
```shell
|
||||
go run .
|
||||
```
|
||||
|
||||
The example:
|
||||
|
||||
1. Creates an index `idx:products` over hashes prefixed with `product:`.
|
||||
2. Seeds 10 products with `category`, `brand`, `price`, `quantity`, `rating`.
|
||||
3. Runs three aggregations:
|
||||
- **Trim before GROUPBY** — `SORTBY @rating DESC MAX 5` → `GROUPBY @category` → `SORTBY @price_total DESC`.
|
||||
- **Multi-stage pipeline** — `LOAD` → `LOAD` → `APPLY` → `SORTBY MAX` → `GROUPBY` → `APPLY` → `SORTBY`.
|
||||
- **AggregateBuilder** — the same first query expressed via the fluent `NewAggregateBuilder` API.
|
||||
@@ -0,0 +1,12 @@
|
||||
module github.com/redis/go-redis/example/search-aggregate-steps
|
||||
|
||||
go 1.24
|
||||
|
||||
replace github.com/redis/go-redis/v9 => ../..
|
||||
|
||||
require github.com/redis/go-redis/v9 v9.20.0
|
||||
|
||||
require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
)
|
||||
@@ -0,0 +1,20 @@
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
||||
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
@@ -0,0 +1,150 @@
|
||||
// Example demonstrating the step-based FT.AGGREGATE API.
|
||||
//
|
||||
// FTAggregateOptions.Steps lets LOAD, APPLY, GROUPBY and SORTBY appear
|
||||
// multiple times in any order. This is useful for shard-level trimming
|
||||
// (SORTBY ... MAX N) before GROUPBY and multi-stage pipelines.
|
||||
//
|
||||
// Requires a Redis instance with the Search module (e.g. Redis 8+).
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
const indexName = "idx:products"
|
||||
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6379",
|
||||
Protocol: 2,
|
||||
})
|
||||
defer rdb.Close()
|
||||
|
||||
if err := rdb.FlushDB(ctx).Err(); err != nil {
|
||||
log.Fatalf("flushdb: %v", err)
|
||||
}
|
||||
createIndex(ctx, rdb)
|
||||
seedProducts(ctx, rdb)
|
||||
|
||||
fmt.Println("# 1. Trim before GROUPBY (real-world use case)")
|
||||
trimBeforeGroup(ctx, rdb)
|
||||
|
||||
fmt.Println("\n# 2. Multi-stage pipeline (LOAD -> APPLY -> SORTBY -> GROUPBY -> APPLY -> SORTBY)")
|
||||
multiStagePipeline(ctx, rdb)
|
||||
|
||||
fmt.Println("\n# 3. Same as #1 but using AggregateBuilder")
|
||||
withBuilder(ctx, rdb)
|
||||
}
|
||||
|
||||
func createIndex(ctx context.Context, rdb *redis.Client) {
|
||||
_, err := rdb.FTCreate(ctx, indexName,
|
||||
&redis.FTCreateOptions{OnHash: true, Prefix: []interface{}{"product:"}},
|
||||
&redis.FieldSchema{FieldName: "category", FieldType: redis.SearchFieldTypeTag},
|
||||
&redis.FieldSchema{FieldName: "brand", FieldType: redis.SearchFieldTypeTag},
|
||||
&redis.FieldSchema{FieldName: "price", FieldType: redis.SearchFieldTypeNumeric, Sortable: true},
|
||||
&redis.FieldSchema{FieldName: "quantity", FieldType: redis.SearchFieldTypeNumeric, Sortable: true},
|
||||
&redis.FieldSchema{FieldName: "rating", FieldType: redis.SearchFieldTypeNumeric, Sortable: true},
|
||||
).Result()
|
||||
if err != nil {
|
||||
log.Fatalf("ftcreate: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func seedProducts(ctx context.Context, rdb *redis.Client) {
|
||||
products := []map[string]interface{}{
|
||||
{"category": "bike", "brand": "Velorim", "price": 270, "quantity": 12, "rating": 4.5},
|
||||
{"category": "bike", "brand": "Velorim", "price": 810, "quantity": 3, "rating": 4.1},
|
||||
{"category": "bike", "brand": "Bicyk", "price": 2300, "quantity": 1, "rating": 4.9},
|
||||
{"category": "bike", "brand": "Bicyk", "price": 430, "quantity": 7, "rating": 3.8},
|
||||
{"category": "helmet", "brand": "Nord", "price": 120, "quantity": 25, "rating": 4.7},
|
||||
{"category": "helmet", "brand": "Nord", "price": 80, "quantity": 40, "rating": 4.0},
|
||||
{"category": "helmet", "brand": "Peaknetic", "price": 200, "quantity": 10, "rating": 4.8},
|
||||
{"category": "light", "brand": "Peaknetic", "price": 45, "quantity": 60, "rating": 4.3},
|
||||
{"category": "light", "brand": "Nord", "price": 30, "quantity": 100, "rating": 3.9},
|
||||
{"category": "light", "brand": "Velorim", "price": 65, "quantity": 35, "rating": 4.6},
|
||||
}
|
||||
for i, p := range products {
|
||||
key := fmt.Sprintf("product:%d", i)
|
||||
if err := rdb.HSet(ctx, key, p).Err(); err != nil {
|
||||
log.Fatalf("hset %s: %v", key, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// trimBeforeGroup shows SORTBY ... MAX N applied BEFORE a GROUPBY
|
||||
// (the pattern that the old pipeline-ordered API could not express).
|
||||
func trimBeforeGroup(ctx context.Context, rdb *redis.Client) {
|
||||
opts := &redis.FTAggregateOptions{
|
||||
Steps: []redis.FTAggregateStep{
|
||||
// Keep only the top 5 highest-rated products per shard.
|
||||
{SortBy: &redis.FTAggregateSortByStep{
|
||||
Fields: []redis.FTAggregateSortBy{{FieldName: "@rating", Desc: true}},
|
||||
Max: 5,
|
||||
}},
|
||||
// Sum their price per category.
|
||||
{GroupBy: &redis.FTAggregateGroupBy{
|
||||
Fields: []interface{}{"@category"},
|
||||
Reduce: []redis.FTAggregateReducer{
|
||||
{Reducer: redis.SearchSum, Args: []interface{}{"@price"}, As: "price_total"},
|
||||
{Reducer: redis.SearchCount, As: "n"},
|
||||
},
|
||||
}},
|
||||
// Sort categories by the aggregated total.
|
||||
{SortBy: &redis.FTAggregateSortByStep{
|
||||
Fields: []redis.FTAggregateSortBy{{FieldName: "@price_total", Desc: true}},
|
||||
}},
|
||||
},
|
||||
}
|
||||
printRows(rdb.FTAggregateWithArgs(ctx, indexName, "*", opts).Result())
|
||||
}
|
||||
|
||||
func multiStagePipeline(ctx context.Context, rdb *redis.Client) {
|
||||
opts := &redis.FTAggregateOptions{
|
||||
Steps: []redis.FTAggregateStep{
|
||||
{Load: &redis.FTAggregateLoad{Field: "@price"}},
|
||||
{Load: &redis.FTAggregateLoad{Field: "@quantity"}},
|
||||
{Apply: &redis.FTAggregateApply{Field: "@price * @quantity", As: "stock_value"}},
|
||||
{SortBy: &redis.FTAggregateSortByStep{
|
||||
Fields: []redis.FTAggregateSortBy{{FieldName: "@stock_value", Desc: true}},
|
||||
Max: 8,
|
||||
}},
|
||||
{GroupBy: &redis.FTAggregateGroupBy{
|
||||
Fields: []interface{}{"@brand"},
|
||||
Reduce: []redis.FTAggregateReducer{
|
||||
{Reducer: redis.SearchSum, Args: []interface{}{"@stock_value"}, As: "brand_value"},
|
||||
},
|
||||
}},
|
||||
{Apply: &redis.FTAggregateApply{Field: "floor(@brand_value)", As: "brand_value"}},
|
||||
{SortBy: &redis.FTAggregateSortByStep{
|
||||
Fields: []redis.FTAggregateSortBy{{FieldName: "@brand_value", Desc: true}},
|
||||
}},
|
||||
},
|
||||
}
|
||||
printRows(rdb.FTAggregateWithArgs(ctx, indexName, "*", opts).Result())
|
||||
}
|
||||
|
||||
func withBuilder(ctx context.Context, rdb *redis.Client) {
|
||||
res, err := rdb.NewAggregateBuilder(ctx, indexName, "*").
|
||||
SortBy("@rating", false).SortByMax(5).
|
||||
GroupBy("@category").
|
||||
ReduceAs(redis.SearchSum, "price_total", "@price").
|
||||
ReduceAs(redis.SearchCount, "n").
|
||||
SortBy("@price_total", false).
|
||||
Run()
|
||||
printRows(res, err)
|
||||
}
|
||||
|
||||
func printRows(res *redis.FTAggregateResult, err error) {
|
||||
if err != nil {
|
||||
log.Fatalf("aggregate: %v", err)
|
||||
}
|
||||
for _, row := range res.Rows {
|
||||
fmt.Printf(" %v\n", row.Fields)
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,77 @@
|
||||
# TLS Certificate Authentication Example
|
||||
|
||||
This example demonstrates how to use TLS client certificates for automatic authentication with Redis 8.6+.
|
||||
|
||||
When Redis is configured with `tls-auth-clients-user CN`, it uses the Common Name (CN) field from the client certificate as the username, eliminating the need for password-based authentication.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Redis 8.6+ with TLS enabled
|
||||
- Redis configured with: `tls-auth-clients-user CN`
|
||||
- Client certificate with CN matching a Redis ACL user
|
||||
- The ACL user must exist and have `nopass` set
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Load CA certificate** - Used to verify the Redis server's certificate
|
||||
2. **Load client certificate** - The CN field must match a Redis ACL username
|
||||
3. **Connect with TLS** - No username/password needed in the connection options
|
||||
4. **Redis authenticates automatically** - Based on the certificate's CN field
|
||||
|
||||
## Running the Example
|
||||
|
||||
```bash
|
||||
# Start Redis with TLS (from the go-redis root directory)
|
||||
docker compose --profile standalone up -d
|
||||
|
||||
# Run the example
|
||||
cd example/tls-cert-auth
|
||||
go run main.go
|
||||
```
|
||||
|
||||
## Expected Output
|
||||
|
||||
```
|
||||
✅ Authenticated as: testcertuser (via TLS certificate CN)
|
||||
✅ SET/GET successful: hello from cert auth!
|
||||
|
||||
🎉 TLS certificate authentication working!
|
||||
```
|
||||
|
||||
## Docker Configuration
|
||||
|
||||
The go-redis test environment is configured with these environment variables:
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
- TLS_ENABLED=yes
|
||||
- TLS_CLIENT_CNS=testcertuser # Generates testcertuser.{crt,key}
|
||||
- TLS_AUTH_CLIENTS_USER=CN # Enables CN-based authentication
|
||||
```
|
||||
|
||||
## Key Code
|
||||
|
||||
```go
|
||||
// Load client certificate (CN must match Redis ACL username)
|
||||
clientCert, err := tls.LoadX509KeyPair(
|
||||
"testcertuser.crt",
|
||||
"testcertuser.key",
|
||||
)
|
||||
|
||||
// Create TLS config
|
||||
tlsConfig := &tls.Config{
|
||||
RootCAs: caCertPool,
|
||||
Certificates: []tls.Certificate{clientCert},
|
||||
}
|
||||
|
||||
// Connect - NO username/password needed!
|
||||
client := redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6666",
|
||||
TLSConfig: tlsConfig,
|
||||
})
|
||||
```
|
||||
|
||||
## Fallback Behavior
|
||||
|
||||
If the certificate CN doesn't match any existing ACL user, Redis falls back to the `default` user. See `tls_cert_auth_test.go` for tests covering both scenarios.
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
module github.com/redis/go-redis/example/tls-cert-auth
|
||||
|
||||
go 1.24
|
||||
|
||||
replace github.com/redis/go-redis/v9 => ../..
|
||||
|
||||
require github.com/redis/go-redis/v9 v9.20.0
|
||||
|
||||
require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
)
|
||||
@@ -0,0 +1,20 @@
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
||||
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
@@ -0,0 +1,116 @@
|
||||
// TLS Certificate Authentication Example
|
||||
//
|
||||
// This example demonstrates how to use TLS client certificates for
|
||||
// automatic authentication with Redis 8.6+.
|
||||
//
|
||||
// When Redis is configured with `tls-auth-clients-user CN`, it uses
|
||||
// the Common Name (CN) field from the client certificate as the username,
|
||||
// eliminating the need for password-based authentication.
|
||||
//
|
||||
// Prerequisites:
|
||||
// - Redis 8.6+ with TLS enabled
|
||||
// - Redis configured with: tls-auth-clients-user CN
|
||||
// - Client certificate with CN matching a Redis ACL user
|
||||
// - The ACL user must exist
|
||||
//
|
||||
// To run with the go-redis test environment:
|
||||
//
|
||||
// docker compose --profile standalone up -d
|
||||
// go run main.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
|
||||
// Configuration - adjust paths as needed
|
||||
certDir := "../../dockers/standalone/tls"
|
||||
username := "testcertuser" // Must match CN in certificate and ACL user
|
||||
tlsPort := "6666"
|
||||
nonTLSPort := "6379"
|
||||
|
||||
// Step 1: First, ensure the ACL user exists (using non-TLS connection)
|
||||
setupClient := redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:" + nonTLSPort,
|
||||
})
|
||||
defer setupClient.Close()
|
||||
|
||||
// Create the ACL user if it doesn't exist
|
||||
err := setupClient.ACLSetUser(ctx,
|
||||
username,
|
||||
"on", // Enable the user
|
||||
"nopass", // No password - will use cert auth
|
||||
"~*", // Access all keys
|
||||
"+@all", // All commands (adjust as needed)
|
||||
).Err()
|
||||
if err != nil {
|
||||
log.Printf("Note: Could not create ACL user (may already exist): %v", err)
|
||||
}
|
||||
|
||||
// Step 2: Load CA certificate
|
||||
caCert, err := os.ReadFile(certDir + "/ca.crt")
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load CA certificate: %v", err)
|
||||
}
|
||||
caCertPool := x509.NewCertPool()
|
||||
caCertPool.AppendCertsFromPEM(caCert)
|
||||
|
||||
// Step 3: Load client certificate (CN must match the Redis ACL username)
|
||||
clientCert, err := tls.LoadX509KeyPair(
|
||||
certDir+"/"+username+".crt",
|
||||
certDir+"/"+username+".key",
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load client certificate: %v", err)
|
||||
}
|
||||
|
||||
// Step 4: Create TLS config
|
||||
tlsConfig := &tls.Config{
|
||||
RootCAs: caCertPool,
|
||||
Certificates: []tls.Certificate{clientCert},
|
||||
ServerName: "localhost",
|
||||
InsecureSkipVerify: true, // Only for self-signed certs in testing
|
||||
}
|
||||
|
||||
// Step 5: Connect to Redis with TLS - NO username/password needed!
|
||||
client := redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:" + tlsPort,
|
||||
TLSConfig: tlsConfig,
|
||||
// Note: No Username or Password fields - auth happens via certificate
|
||||
})
|
||||
defer client.Close()
|
||||
|
||||
// Step 6: Verify authentication
|
||||
whoami, err := client.ACLWhoAmI(ctx).Result()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to get current user: %v", err)
|
||||
}
|
||||
fmt.Printf("✅ Authenticated as: %s (via TLS certificate CN)\n", whoami)
|
||||
|
||||
// Step 7: Test some commands
|
||||
err = client.Set(ctx, "tls-auth-example", "hello from cert auth!", 0).Err()
|
||||
if err != nil {
|
||||
log.Fatalf("SET failed: %v", err)
|
||||
}
|
||||
|
||||
val, err := client.Get(ctx, "tls-auth-example").Result()
|
||||
if err != nil {
|
||||
log.Fatalf("GET failed: %v", err)
|
||||
}
|
||||
fmt.Printf("✅ SET/GET successful: %s\n", val)
|
||||
|
||||
// Cleanup
|
||||
client.Del(ctx, "tls-auth-example")
|
||||
|
||||
fmt.Println("\n🎉 TLS certificate authentication working!")
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
# TLS Connection Examples
|
||||
|
||||
Shows different ways to connect to Redis over TLS.
|
||||
|
||||
## Running
|
||||
|
||||
Start Redis with TLS:
|
||||
|
||||
```shell
|
||||
cd ../..
|
||||
docker compose --profile standalone up -d
|
||||
```
|
||||
|
||||
Then run the example:
|
||||
|
||||
```shell
|
||||
go run .
|
||||
```
|
||||
|
||||
## Connection Methods
|
||||
|
||||
### 1. InsecureSkipVerify (for testing)
|
||||
|
||||
Quick way to test with self-signed certs:
|
||||
|
||||
```go
|
||||
client := redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6666",
|
||||
TLSConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Don't use this in production.
|
||||
|
||||
### 2. With CA certificate
|
||||
|
||||
Proper way for production:
|
||||
|
||||
```go
|
||||
caCert, _ := os.ReadFile("path/to/ca.crt")
|
||||
caCertPool := x509.NewCertPool()
|
||||
caCertPool.AppendCertsFromPEM(caCert)
|
||||
|
||||
client := redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6666",
|
||||
TLSConfig: &tls.Config{
|
||||
RootCAs: caCertPool,
|
||||
ServerName: "localhost",
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### 3. Mutual TLS
|
||||
|
||||
If Redis requires client certs:
|
||||
|
||||
```go
|
||||
caCert, _ := os.ReadFile("path/to/ca.crt")
|
||||
caCertPool := x509.NewCertPool()
|
||||
caCertPool.AppendCertsFromPEM(caCert)
|
||||
|
||||
cert, _ := tls.LoadX509KeyPair("path/to/client.crt", "path/to/client.key")
|
||||
|
||||
client := redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6666",
|
||||
TLSConfig: &tls.Config{
|
||||
RootCAs: caCertPool,
|
||||
Certificates: []tls.Certificate{cert},
|
||||
ServerName: "localhost",
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### 4. Using rediss:// URL
|
||||
|
||||
```go
|
||||
opt, _ := redis.ParseURL("rediss://localhost:6666")
|
||||
opt.TLSConfig = &tls.Config{
|
||||
InsecureSkipVerify: true, // for testing only
|
||||
}
|
||||
client := redis.NewClient(opt)
|
||||
```
|
||||
|
||||
### 5. Certificate-based auth
|
||||
|
||||
Redis 6.2+ can authenticate users based on the certificate CN field. You need to configure Redis with:
|
||||
|
||||
```
|
||||
tls-auth-clients optional
|
||||
tls-auth-clients-user CN
|
||||
```
|
||||
|
||||
Then the CN in your client cert becomes your username - no password needed.
|
||||
|
||||
Check `../../tls_cert_auth_test.go` for a working example that:
|
||||
- Generates a client cert with a specific CN
|
||||
- Connects to Redis with that cert
|
||||
- Verifies auth based on the CN
|
||||
|
||||
Note: Current Redis test build doesn't support this yet, so the test skips gracefully.
|
||||
|
||||
## Tests
|
||||
|
||||
Run the TLS tests:
|
||||
|
||||
```shell
|
||||
go test -v -run "^TestTLS" -timeout 30s
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Always verify certs in production (don't use InsecureSkipVerify)
|
||||
- Keep your private keys safe
|
||||
- Use TLS 1.2 or higher
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
module github.com/redis/go-redis/example/tls-connection
|
||||
|
||||
go 1.24
|
||||
|
||||
replace github.com/redis/go-redis/v9 => ../..
|
||||
|
||||
require github.com/redis/go-redis/v9 v9.20.0
|
||||
|
||||
require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
)
|
||||
@@ -0,0 +1,20 @@
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
||||
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
@@ -0,0 +1,123 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
|
||||
// Example 1: TLS with InsecureSkipVerify (for testing with self-signed certs)
|
||||
fmt.Println("Example 1: TLS with InsecureSkipVerify")
|
||||
client1 := redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6666", // TLS port
|
||||
TLSConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
})
|
||||
defer client1.Close()
|
||||
|
||||
if err := client1.Ping(ctx).Err(); err != nil {
|
||||
fmt.Printf("Failed to connect: %v\n", err)
|
||||
} else {
|
||||
fmt.Println("✅ Connected successfully with InsecureSkipVerify")
|
||||
}
|
||||
|
||||
// Example 2: TLS with CA certificate verification
|
||||
fmt.Println("\nExample 2: TLS with CA certificate verification")
|
||||
|
||||
// Load CA certificate
|
||||
caCert, err := os.ReadFile("path/to/ca.crt")
|
||||
if err != nil {
|
||||
fmt.Printf("Note: CA cert not found (this is expected in this example): %v\n", err)
|
||||
} else {
|
||||
caCertPool := x509.NewCertPool()
|
||||
caCertPool.AppendCertsFromPEM(caCert)
|
||||
|
||||
client2 := redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6666",
|
||||
TLSConfig: &tls.Config{
|
||||
RootCAs: caCertPool,
|
||||
ServerName: "localhost",
|
||||
},
|
||||
})
|
||||
defer client2.Close()
|
||||
|
||||
if err := client2.Ping(ctx).Err(); err != nil {
|
||||
fmt.Printf("Failed to connect: %v\n", err)
|
||||
} else {
|
||||
fmt.Println("✅ Connected successfully with CA verification")
|
||||
}
|
||||
}
|
||||
|
||||
// Example 3: TLS with client certificate (mutual TLS)
|
||||
fmt.Println("\nExample 3: TLS with client certificate (mutual TLS)")
|
||||
|
||||
// Load CA certificate
|
||||
caCert, err = os.ReadFile("path/to/ca.crt")
|
||||
if err != nil {
|
||||
fmt.Printf("Note: CA cert not found (this is expected in this example): %v\n", err)
|
||||
} else {
|
||||
caCertPool := x509.NewCertPool()
|
||||
caCertPool.AppendCertsFromPEM(caCert)
|
||||
|
||||
// Load client certificate and key
|
||||
cert, err := tls.LoadX509KeyPair("path/to/client.crt", "path/to/client.key")
|
||||
if err != nil {
|
||||
fmt.Printf("Note: Client cert not found (this is expected in this example): %v\n", err)
|
||||
} else {
|
||||
client3 := redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6666",
|
||||
TLSConfig: &tls.Config{
|
||||
RootCAs: caCertPool,
|
||||
Certificates: []tls.Certificate{cert},
|
||||
ServerName: "localhost",
|
||||
},
|
||||
})
|
||||
defer client3.Close()
|
||||
|
||||
if err := client3.Ping(ctx).Err(); err != nil {
|
||||
fmt.Printf("Failed to connect: %v\n", err)
|
||||
} else {
|
||||
fmt.Println("✅ Connected successfully with client certificate")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Example 4: Using rediss:// URL scheme
|
||||
fmt.Println("\nExample 4: Using rediss:// URL scheme")
|
||||
|
||||
opt, err := redis.ParseURL("rediss://localhost:6666")
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to parse URL: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Add InsecureSkipVerify for testing with self-signed certs
|
||||
opt.TLSConfig = &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
}
|
||||
|
||||
client4 := redis.NewClient(opt)
|
||||
defer client4.Close()
|
||||
|
||||
if err := client4.Ping(ctx).Err(); err != nil {
|
||||
fmt.Printf("Failed to connect: %v\n", err)
|
||||
} else {
|
||||
fmt.Println("✅ Connected successfully using rediss:// URL")
|
||||
}
|
||||
|
||||
// Example 5: TLS with certificate-based authentication (future feature)
|
||||
// This demonstrates how to use client certificates for authentication
|
||||
// when Redis is configured with: tls-auth-clients-user CN
|
||||
fmt.Println("\nExample 5: TLS with certificate-based authentication")
|
||||
fmt.Println("Note: This requires Redis 6.2+ with tls-auth-clients-user CN configuration")
|
||||
fmt.Println("The certificate's CN (Common Name) field will be used as the Redis username")
|
||||
fmt.Println("See tls_cert_auth_test.go for a complete working example")
|
||||
}
|
||||
@@ -57,8 +57,8 @@ func Example_instrumentation() {
|
||||
// finished dialing tcp :6379
|
||||
// starting processing: <[hello 3]>
|
||||
// finished processing: <[hello 3]>
|
||||
// starting processing: <[client maint_notifications on moving-endpoint-type internal-fqdn]>
|
||||
// finished processing: <[client maint_notifications on moving-endpoint-type internal-fqdn]>
|
||||
// starting processing: <[client maint_notifications on moving-endpoint-type internal-ip]>
|
||||
// finished processing: <[client maint_notifications on moving-endpoint-type internal-ip]>
|
||||
// finished processing: <[ping]>
|
||||
}
|
||||
|
||||
@@ -80,8 +80,8 @@ func ExamplePipeline_instrumentation() {
|
||||
// finished dialing tcp :6379
|
||||
// starting processing: <[hello 3]>
|
||||
// finished processing: <[hello 3]>
|
||||
// starting processing: <[client maint_notifications on moving-endpoint-type internal-fqdn]>
|
||||
// finished processing: <[client maint_notifications on moving-endpoint-type internal-fqdn]>
|
||||
// starting processing: <[client maint_notifications on moving-endpoint-type internal-ip]>
|
||||
// finished processing: <[client maint_notifications on moving-endpoint-type internal-ip]>
|
||||
// pipeline finished processing: [[ping] [ping]]
|
||||
}
|
||||
|
||||
@@ -103,8 +103,8 @@ func ExampleClient_Watch_instrumentation() {
|
||||
// finished dialing tcp :6379
|
||||
// starting processing: <[hello 3]>
|
||||
// finished processing: <[hello 3]>
|
||||
// starting processing: <[client maint_notifications on moving-endpoint-type internal-fqdn]>
|
||||
// finished processing: <[client maint_notifications on moving-endpoint-type internal-fqdn]>
|
||||
// starting processing: <[client maint_notifications on moving-endpoint-type internal-ip]>
|
||||
// finished processing: <[client maint_notifications on moving-endpoint-type internal-ip]>
|
||||
// finished processing: <[watch foo]>
|
||||
// starting processing: <[ping]>
|
||||
// finished processing: <[ping]>
|
||||
|
||||
+2
-2
@@ -58,7 +58,7 @@ func ExampleParseURL() {
|
||||
}
|
||||
|
||||
func ExampleNewFailoverClient() {
|
||||
// See http://redis.io/topics/sentinel for instructions how to
|
||||
// See https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel for instructions how to
|
||||
// setup Redis Sentinel.
|
||||
rdb := redis.NewFailoverClient(&redis.FailoverOptions{
|
||||
MasterName: "master",
|
||||
@@ -68,7 +68,7 @@ func ExampleNewFailoverClient() {
|
||||
}
|
||||
|
||||
func ExampleNewClusterClient() {
|
||||
// See http://redis.io/topics/cluster-tutorial for instructions
|
||||
// See https://redis.io/docs/latest/operate/oss_and_stack/management/scaling for instructions
|
||||
// how to setup Redis Cluster.
|
||||
rdb := redis.NewClusterClient(&redis.ClusterOptions{
|
||||
Addrs: []string{":7000", ":7001", ":7002", ":7003", ":7004", ":7005"},
|
||||
|
||||
@@ -106,3 +106,27 @@ func (c *ModuleLoadexConfig) ToArgs() []interface{} {
|
||||
func ShouldRetry(err error, retryTimeout bool) bool {
|
||||
return shouldRetry(err, retryTimeout)
|
||||
}
|
||||
|
||||
// SentinelFailoverHasSentinel returns true if the sentinelFailover
|
||||
// still holds a live sentinel connection. Used in tests to verify
|
||||
// that replicaAddrs does not tear down the sentinel for master-only
|
||||
// (zero-replica) setups.
|
||||
func (c *sentinelFailover) HasSentinel() bool {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return c.sentinel != nil
|
||||
}
|
||||
|
||||
// NewTestSentinelFailover creates a sentinelFailover with minimal
|
||||
// options for unit testing. The caller must set c.sentinel manually.
|
||||
func NewTestSentinelFailover(opt *FailoverOptions, sentinelAddrs []string) *sentinelFailover {
|
||||
return &sentinelFailover{
|
||||
opt: opt,
|
||||
sentinelAddrs: sentinelAddrs,
|
||||
}
|
||||
}
|
||||
|
||||
// ReplicaAddrs is exported for testing.
|
||||
func (c *sentinelFailover) ReplicaAddrs(ctx context.Context) ([]string, error) {
|
||||
return c.replicaAddrs(ctx, false)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
Copyright (c) 2013 The github.com/redis/go-redis Authors.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -14,7 +14,6 @@ require (
|
||||
|
||||
require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
)
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
@@ -10,8 +12,6 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
@@ -37,6 +37,8 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
@@ -47,6 +49,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
||||
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
|
||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
@@ -71,6 +75,8 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
Copyright (c) 2013 The github.com/redis/go-redis Authors.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -12,7 +12,6 @@ require (
|
||||
|
||||
require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
)
|
||||
|
||||
|
||||
@@ -5,9 +5,16 @@ github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
||||
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
Copyright (c) 2013 The github.com/redis/go-redis Authors.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -0,0 +1,67 @@
|
||||
package redisotel
|
||||
|
||||
// OpenTelemetry semantic convention attribute keys for Redis client metrics.
|
||||
// These constants follow the OTel semantic conventions for database clients.
|
||||
// Reference: https://opentelemetry.io/docs/specs/semconv/database/redis/
|
||||
|
||||
const (
|
||||
// Database semantic convention attributes
|
||||
AttrDBSystemName = "db.system.name"
|
||||
AttrDBNamespace = "db.namespace"
|
||||
AttrDBOperationName = "db.operation.name"
|
||||
AttrDBOperationBatchSize = "db.operation.batch.size"
|
||||
AttrDBResponseStatusCode = "db.response.status_code"
|
||||
|
||||
// Connection pool attributes
|
||||
AttrDBClientConnectionPoolName = "db.client.connection.pool.name"
|
||||
AttrDBClientConnectionState = "db.client.connection.state"
|
||||
|
||||
// Server attributes
|
||||
AttrServerAddress = "server.address"
|
||||
AttrServerPort = "server.port"
|
||||
|
||||
// Network attributes
|
||||
AttrNetworkPeerAddress = "network.peer.address"
|
||||
AttrNetworkPeerPort = "network.peer.port"
|
||||
|
||||
// Error attributes
|
||||
AttrErrorType = "error.type"
|
||||
|
||||
// Redis-specific attributes
|
||||
AttrRedisClientLibrary = "redis.client.library"
|
||||
AttrRedisClientConnectionPubSub = "redis.client.connection.pubsub"
|
||||
AttrRedisClientConnectionCloseReason = "redis.client.connection.close.reason"
|
||||
AttrRedisClientErrorsCategory = "redis.client.errors.category"
|
||||
AttrRedisClientErrorsInternal = "redis.client.errors.internal"
|
||||
AttrRedisClientOperationRetryAttempts = "redis.client.operation.retry_attempts"
|
||||
|
||||
// PubSub attributes
|
||||
AttrRedisClientPubSubDirection = "redis.client.pubsub.direction"
|
||||
AttrRedisClientPubSubSharded = "redis.client.pubsub.sharded"
|
||||
AttrRedisClientPubSubChannel = "redis.client.pubsub.channel"
|
||||
|
||||
// Stream attributes
|
||||
AttrRedisClientStreamName = "redis.client.stream.name"
|
||||
AttrRedisClientStreamConsumerGroup = "redis.client.stream.consumer_group"
|
||||
AttrRedisClientStreamConsumerName = "redis.client.stream.consumer_name"
|
||||
|
||||
// Notification attributes
|
||||
AttrRedisClientConnectionNotification = "redis.client.connection.notification"
|
||||
)
|
||||
|
||||
// Connection state values
|
||||
const (
|
||||
ConnectionStateIdle = "idle"
|
||||
ConnectionStateUsed = "used"
|
||||
)
|
||||
|
||||
// PubSub direction values
|
||||
const (
|
||||
PubSubDirectionSent = "sent"
|
||||
PubSubDirectionReceived = "received"
|
||||
)
|
||||
|
||||
// DB system value
|
||||
const (
|
||||
DBSystemRedis = "redis"
|
||||
)
|
||||
@@ -0,0 +1,267 @@
|
||||
package redisotel
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
)
|
||||
|
||||
type MetricGroup string
|
||||
|
||||
const (
|
||||
MetricGroupCommand MetricGroup = "command"
|
||||
MetricGroupConnectionBasic MetricGroup = "connection-basic"
|
||||
MetricGroupResiliency MetricGroup = "resiliency"
|
||||
MetricGroupConnectionAdvanced MetricGroup = "connection-advanced"
|
||||
MetricGroupPubSub MetricGroup = "pubsub"
|
||||
MetricGroupStream MetricGroup = "stream"
|
||||
)
|
||||
|
||||
type HistogramAggregation string
|
||||
|
||||
const (
|
||||
HistogramAggregationExplicitBucket HistogramAggregation = "explicit_bucket_histogram"
|
||||
HistogramAggregationBase2Exponential HistogramAggregation = "base2_exponential_bucket_histogram"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
// Core settings
|
||||
meterProvider metric.MeterProvider
|
||||
enabled bool
|
||||
|
||||
// Metric group settings
|
||||
enabledMetricGroups map[MetricGroup]bool
|
||||
|
||||
// Command filtering
|
||||
includeCommands map[string]bool // nil means include all
|
||||
excludeCommands map[string]bool // nil means exclude none
|
||||
|
||||
// Cardinality reduction
|
||||
hidePubSubChannelNames bool
|
||||
hideStreamNames bool
|
||||
|
||||
// Histogram settings
|
||||
histAggregation HistogramAggregation
|
||||
|
||||
// Bucket configurations for different histogram metrics
|
||||
bucketsOperationDuration []float64
|
||||
bucketsStreamProcessingDuration []float64
|
||||
bucketsConnectionCreateTime []float64
|
||||
bucketsConnectionWaitTime []float64
|
||||
}
|
||||
|
||||
func (c *config) isMetricGroupEnabled(group MetricGroup) bool {
|
||||
return c.enabledMetricGroups[group]
|
||||
}
|
||||
|
||||
func (c *config) isCommandIncluded(command string) bool {
|
||||
command = strings.ToLower(command)
|
||||
if c.excludeCommands != nil && c.excludeCommands[command] {
|
||||
return false
|
||||
}
|
||||
|
||||
if c.includeCommands != nil {
|
||||
return c.includeCommands[command]
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// defaultHistogramBuckets returns the default histogram buckets for all duration metrics.
|
||||
// These buckets are designed to capture typical Redis operation and connection latencies:
|
||||
// - Sub-millisecond: 0.0001s (0.1ms), 0.0005s (0.5ms)
|
||||
// - Milliseconds: 0.001s (1ms), 0.005s (5ms), 0.01s (10ms), 0.05s (50ms), 0.1s (100ms)
|
||||
// - Sub-second: 0.5s (500ms)
|
||||
// - Seconds: 1s, 5s, 10s
|
||||
//
|
||||
// This covers the range from 0.1ms to 10s, which is suitable for:
|
||||
// - db.client.operation.duration (command execution time)
|
||||
// - db.client.connection.create_time (connection establishment)
|
||||
// - db.client.connection.wait_time (waiting for connection from pool)
|
||||
// - redis.client.stream.processing_duration (stream message processing)
|
||||
func defaultHistogramBuckets() []float64 {
|
||||
return []float64{
|
||||
0.0001, // 0.1ms
|
||||
0.0005, // 0.5ms
|
||||
0.001, // 1ms
|
||||
0.005, // 5ms
|
||||
0.01, // 10ms
|
||||
0.05, // 50ms
|
||||
0.1, // 100ms
|
||||
0.5, // 500ms
|
||||
1.0, // 1s
|
||||
5.0, // 5s
|
||||
10.0, // 10s
|
||||
}
|
||||
}
|
||||
|
||||
// MetricGroupFlags represents metric groups as bitwise flags
|
||||
type MetricGroupFlags uint32
|
||||
|
||||
const (
|
||||
MetricGroupFlagCommand MetricGroupFlags = 1 << 0
|
||||
MetricGroupFlagConnectionBasic MetricGroupFlags = 1 << 1
|
||||
MetricGroupFlagResiliency MetricGroupFlags = 1 << 2
|
||||
MetricGroupFlagConnectionAdvanced MetricGroupFlags = 1 << 3
|
||||
MetricGroupFlagPubSub MetricGroupFlags = 1 << 4
|
||||
MetricGroupFlagStream MetricGroupFlags = 1 << 5
|
||||
|
||||
// MetricGroupAll enables all metric groups
|
||||
MetricGroupAll MetricGroupFlags = MetricGroupFlagCommand |
|
||||
MetricGroupFlagConnectionBasic |
|
||||
MetricGroupFlagResiliency |
|
||||
MetricGroupFlagConnectionAdvanced |
|
||||
MetricGroupFlagPubSub |
|
||||
MetricGroupFlagStream
|
||||
)
|
||||
|
||||
// Use NewConfig() to create a new instance with defaults, then chain
|
||||
// builder methods to customize.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// config := redisotel.NewConfig().
|
||||
// WithEnabled(true).
|
||||
// WithMetricGroups(redisotel.MetricGroupAll).
|
||||
// WithMeterProvider(myProvider)
|
||||
//
|
||||
// otel := redisotel.GetObservabilityInstance()
|
||||
// otel.Init(config)
|
||||
type Config struct {
|
||||
// Core settings
|
||||
Enabled bool
|
||||
MeterProvider metric.MeterProvider
|
||||
|
||||
// Metric groups (bitwise flags)
|
||||
MetricGroups MetricGroupFlags
|
||||
|
||||
// Command filtering
|
||||
IncludeCommands map[string]bool // nil means include all
|
||||
ExcludeCommands map[string]bool // nil means exclude none
|
||||
|
||||
// Cardinality reduction
|
||||
HidePubSubChannelNames bool
|
||||
HideStreamNames bool
|
||||
|
||||
// Histogram settings
|
||||
HistogramAggregation HistogramAggregation
|
||||
|
||||
// Bucket configurations for different histogram metrics
|
||||
BucketsOperationDuration []float64
|
||||
BucketsStreamLag []float64
|
||||
BucketsConnectionCreateTime []float64
|
||||
BucketsConnectionWaitTime []float64
|
||||
}
|
||||
|
||||
// NewConfig creates a new Config with default values.
|
||||
// Default configuration:
|
||||
// - Enabled: false (must explicitly enable)
|
||||
// - MetricGroups: all metric groups (command, connection, resiliency, pubsub, stream)
|
||||
// - HistogramAggregation: explicit bucket
|
||||
// - Buckets: 0.1ms to 10s (suitable for Redis operations)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// config := redisotel.NewConfig().
|
||||
// WithEnabled(true)
|
||||
//
|
||||
// To disable specific metric groups, use WithMetricGroups:
|
||||
//
|
||||
// config := redisotel.NewConfig().
|
||||
// WithEnabled(true).
|
||||
// WithMetricGroups(redisotel.MetricGroupFlagConnectionBasic | redisotel.MetricGroupFlagResiliency)
|
||||
func NewConfig() *Config {
|
||||
return &Config{
|
||||
Enabled: false,
|
||||
MeterProvider: nil, // Will use global otel.GetMeterProvider() if nil
|
||||
|
||||
// Default metric groups: all groups enabled for comprehensive observability
|
||||
MetricGroups: MetricGroupAll,
|
||||
|
||||
// No command filtering by default
|
||||
IncludeCommands: nil,
|
||||
ExcludeCommands: nil,
|
||||
|
||||
// Don't hide labels by default
|
||||
HidePubSubChannelNames: false,
|
||||
HideStreamNames: false,
|
||||
|
||||
// Use explicit bucket histogram by default
|
||||
HistogramAggregation: HistogramAggregationExplicitBucket,
|
||||
|
||||
// Default buckets for all duration metrics
|
||||
BucketsOperationDuration: defaultHistogramBuckets(),
|
||||
BucketsStreamLag: defaultHistogramBuckets(),
|
||||
BucketsConnectionCreateTime: defaultHistogramBuckets(),
|
||||
BucketsConnectionWaitTime: defaultHistogramBuckets(),
|
||||
}
|
||||
}
|
||||
|
||||
// WithEnabled enables or disables metrics emission.
|
||||
// Default: false (must explicitly enable)
|
||||
func (c *Config) WithEnabled(enabled bool) *Config {
|
||||
c.Enabled = enabled
|
||||
return c
|
||||
}
|
||||
|
||||
// WithMeterProvider sets the meter provider to use for creating metrics.
|
||||
// If not provided, the global meter provider from otel.GetMeterProvider() will be used.
|
||||
func (c *Config) WithMeterProvider(provider metric.MeterProvider) *Config {
|
||||
c.MeterProvider = provider
|
||||
return c
|
||||
}
|
||||
|
||||
// WithMetricGroups sets which metric groups to register using bitwise flags.
|
||||
// You can combine multiple groups with the | operator.
|
||||
func (c *Config) WithMetricGroups(groups MetricGroupFlags) *Config {
|
||||
c.MetricGroups = groups
|
||||
return c
|
||||
}
|
||||
|
||||
// WithIncludeCommands sets a command allow-list for metrics.
|
||||
func (c *Config) WithIncludeCommands(commands []string) *Config {
|
||||
c.IncludeCommands = make(map[string]bool)
|
||||
for _, cmd := range commands {
|
||||
c.IncludeCommands[strings.ToLower(cmd)] = true
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// WithExcludeCommands sets a command deny-list for metrics.
|
||||
// Commands in this list will not have metrics recorded.
|
||||
func (c *Config) WithExcludeCommands(commands []string) *Config {
|
||||
c.ExcludeCommands = make(map[string]bool)
|
||||
for _, cmd := range commands {
|
||||
c.ExcludeCommands[strings.ToLower(cmd)] = true
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// WithHidePubSubChannelNames omits channel label from Pub/Sub metrics to reduce cardinality.
|
||||
func (c *Config) WithHidePubSubChannelNames(hide bool) *Config {
|
||||
c.HidePubSubChannelNames = hide
|
||||
return c
|
||||
}
|
||||
|
||||
// WithHideStreamNames omits stream label from stream metrics to reduce cardinality.
|
||||
func (c *Config) WithHideStreamNames(hide bool) *Config {
|
||||
c.HideStreamNames = hide
|
||||
return c
|
||||
}
|
||||
|
||||
// WithHistogramAggregation sets the histogram aggregation mode.
|
||||
func (c *Config) WithHistogramAggregation(agg HistogramAggregation) *Config {
|
||||
c.HistogramAggregation = agg
|
||||
return c
|
||||
}
|
||||
|
||||
// WithHistogramBuckets sets custom histogram buckets for ALL duration metrics.
|
||||
// If not set, uses defaultHistogramBuckets() which covers 0.1ms to 10s.
|
||||
// Buckets should be in seconds (e.g., 0.001 = 1ms, 0.1 = 100ms, 1.0 = 1s).
|
||||
func (c *Config) WithHistogramBuckets(buckets []float64) *Config {
|
||||
c.BucketsOperationDuration = buckets
|
||||
c.BucketsStreamLag = buckets
|
||||
c.BucketsConnectionCreateTime = buckets
|
||||
c.BucketsConnectionWaitTime = buckets
|
||||
return c
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
module github.com/redis/go-redis/extra/redisotel-native/v9
|
||||
|
||||
go 1.25.0
|
||||
|
||||
replace github.com/redis/go-redis/v9 => ../..
|
||||
|
||||
require (
|
||||
github.com/redis/go-redis/v9 v9.20.0
|
||||
go.opentelemetry.io/otel v1.43.0
|
||||
go.opentelemetry.io/otel/metric v1.43.0
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
)
|
||||
@@ -0,0 +1,43 @@
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
||||
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
||||
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -0,0 +1,916 @@
|
||||
package redisotel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
)
|
||||
|
||||
// timeoutError is an interface for errors that have a Timeout() method
|
||||
type timeoutError interface {
|
||||
Timeout() bool
|
||||
}
|
||||
|
||||
const (
|
||||
// Library name for redis.client.library attribute
|
||||
libraryName = "go-redis"
|
||||
)
|
||||
|
||||
// getLibraryVersionAttr returns the redis.client.library attribute
|
||||
func getLibraryVersionAttr() attribute.KeyValue {
|
||||
return attribute.String(AttrRedisClientLibrary, fmt.Sprintf("%s:%s", libraryName, redis.Version()))
|
||||
}
|
||||
|
||||
// addServerPortIfNonDefault adds server.port attribute if port is not the default (6379)
|
||||
func addServerPortIfNonDefault(attrs []attribute.KeyValue, serverPort string) []attribute.KeyValue {
|
||||
if serverPort != "" && serverPort != "6379" {
|
||||
return append(attrs, attribute.String(AttrServerPort, serverPort))
|
||||
}
|
||||
return attrs
|
||||
}
|
||||
|
||||
// metricsRecorder implements the otel.Recorder interface
|
||||
type metricsRecorder struct {
|
||||
operationDuration metric.Float64Histogram
|
||||
connectionCount metric.Int64UpDownCounter // OTel semconv: UpDownCounter for connection.count
|
||||
connectionCreateTime metric.Float64Histogram
|
||||
connectionRelaxedTimeout metric.Int64UpDownCounter
|
||||
connectionHandoff metric.Int64Counter
|
||||
clientErrors metric.Int64Counter
|
||||
maintenanceNotifications metric.Int64Counter
|
||||
|
||||
connectionWaitTime metric.Float64Histogram
|
||||
connectionClosed metric.Int64Counter
|
||||
connectionPendingReqs metric.Int64UpDownCounter // OTel semconv: UpDownCounter for pending_requests
|
||||
|
||||
pubsubMessages metric.Int64Counter
|
||||
|
||||
streamLag metric.Float64Histogram
|
||||
|
||||
// Configuration
|
||||
cfg *config
|
||||
}
|
||||
|
||||
// RecordOperationDuration records db.client.operation.duration metric
|
||||
func (r *metricsRecorder) RecordOperationDuration(
|
||||
ctx context.Context,
|
||||
duration time.Duration,
|
||||
cmd redis.Cmder,
|
||||
attempts int,
|
||||
err error,
|
||||
cn redis.ConnInfo,
|
||||
dbIndex int,
|
||||
) {
|
||||
if r.operationDuration == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if command should be included
|
||||
if r.cfg != nil && !r.cfg.isCommandIncluded(cmd.Name()) {
|
||||
return
|
||||
}
|
||||
|
||||
// Convert duration to seconds (OTel convention for duration metrics)
|
||||
durationSeconds := duration.Seconds()
|
||||
|
||||
serverAddr, serverPort := extractServerInfo(cn)
|
||||
|
||||
// Build attributes
|
||||
attrs := []attribute.KeyValue{
|
||||
// Required attributes
|
||||
attribute.String(AttrDBOperationName, cmd.FullName()),
|
||||
getLibraryVersionAttr(),
|
||||
attribute.Int(AttrRedisClientOperationRetryAttempts, attempts-1), // attempts-1 = retry count
|
||||
|
||||
// Recommended attributes
|
||||
attribute.String(AttrDBSystemName, DBSystemRedis),
|
||||
attribute.String(AttrServerAddress, serverAddr),
|
||||
attribute.String(AttrDBNamespace, strconv.Itoa(dbIndex)),
|
||||
}
|
||||
|
||||
// Add server.port if not default
|
||||
attrs = addServerPortIfNonDefault(attrs, serverPort)
|
||||
|
||||
// Add network.peer.address and network.peer.port from connection
|
||||
if cn != nil {
|
||||
remoteAddr := cn.RemoteAddr()
|
||||
if remoteAddr != nil {
|
||||
peerAddr, peerPort := splitHostPort(remoteAddr.String())
|
||||
if peerAddr != "" {
|
||||
attrs = append(attrs, attribute.String(AttrNetworkPeerAddress, peerAddr))
|
||||
}
|
||||
if peerPort != "" {
|
||||
attrs = append(attrs, attribute.String(AttrNetworkPeerPort, peerPort))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
attrs = append(attrs, attribute.String(AttrErrorType, classifyError(err)))
|
||||
attrs = append(attrs, attribute.String(AttrRedisClientErrorsCategory, getErrorCategory(err)))
|
||||
if statusCode := extractRedisErrorPrefix(err); statusCode != "" {
|
||||
attrs = append(attrs, attribute.String(AttrDBResponseStatusCode, statusCode))
|
||||
}
|
||||
}
|
||||
|
||||
// Record the histogram
|
||||
r.operationDuration.Record(ctx, durationSeconds, metric.WithAttributes(attrs...))
|
||||
}
|
||||
|
||||
// RecordPipelineOperationDuration records db.client.operation.duration metric for pipelines/transactions.
|
||||
// operationName should be "PIPELINE" for regular pipelines or "MULTI" for transactions.
|
||||
func (r *metricsRecorder) RecordPipelineOperationDuration(
|
||||
ctx context.Context,
|
||||
duration time.Duration,
|
||||
operationName string,
|
||||
cmdCount int,
|
||||
attempts int,
|
||||
err error,
|
||||
cn redis.ConnInfo,
|
||||
dbIndex int,
|
||||
) {
|
||||
if r.operationDuration == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Convert duration to seconds (OTel convention for duration metrics)
|
||||
durationSeconds := duration.Seconds()
|
||||
|
||||
// Extract server info from connection
|
||||
serverAddr, serverPort := extractServerInfo(cn)
|
||||
|
||||
// Build attributes
|
||||
attrs := []attribute.KeyValue{
|
||||
attribute.String(AttrDBOperationName, operationName),
|
||||
getLibraryVersionAttr(),
|
||||
attribute.Int(AttrRedisClientOperationRetryAttempts, attempts-1), // attempts-1 = retry count
|
||||
attribute.Int(AttrDBOperationBatchSize, cmdCount), // number of commands in pipeline
|
||||
attribute.String(AttrDBSystemName, DBSystemRedis),
|
||||
attribute.String(AttrServerAddress, serverAddr),
|
||||
attribute.String(AttrDBNamespace, strconv.Itoa(dbIndex)),
|
||||
}
|
||||
|
||||
// Add server.port if not default
|
||||
attrs = addServerPortIfNonDefault(attrs, serverPort)
|
||||
|
||||
// Add network.peer.address and network.peer.port from connection
|
||||
if cn != nil {
|
||||
remoteAddr := cn.RemoteAddr()
|
||||
if remoteAddr != nil {
|
||||
peerAddr, peerPort := splitHostPort(remoteAddr.String())
|
||||
if peerAddr != "" {
|
||||
attrs = append(attrs, attribute.String(AttrNetworkPeerAddress, peerAddr))
|
||||
}
|
||||
if peerPort != "" {
|
||||
attrs = append(attrs, attribute.String(AttrNetworkPeerPort, peerPort))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add error attributes if pipeline failed
|
||||
if err != nil {
|
||||
attrs = append(attrs, attribute.String(AttrErrorType, classifyError(err)))
|
||||
attrs = append(attrs, attribute.String(AttrRedisClientErrorsCategory, getErrorCategory(err)))
|
||||
if statusCode := extractRedisErrorPrefix(err); statusCode != "" {
|
||||
attrs = append(attrs, attribute.String(AttrDBResponseStatusCode, statusCode))
|
||||
}
|
||||
}
|
||||
|
||||
// Record the histogram
|
||||
r.operationDuration.Record(ctx, durationSeconds, metric.WithAttributes(attrs...))
|
||||
}
|
||||
|
||||
// classifyError returns the error.type attribute value
|
||||
// Format: <category>:<subcategory>:<error_name>
|
||||
func classifyError(err error) string {
|
||||
if err == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Timeout errors
|
||||
if isTimeoutError(err) {
|
||||
return "timeout"
|
||||
}
|
||||
|
||||
// Network errors - normalize to avoid high cardinality
|
||||
if isNetworkError(err) {
|
||||
return normalizeNetworkError(err)
|
||||
}
|
||||
|
||||
// Redis errors (start with error prefix like ERR, WRONGTYPE, etc.)
|
||||
if prefix := extractRedisErrorPrefix(err); prefix != "" {
|
||||
return fmt.Sprintf("redis:%s", prefix)
|
||||
}
|
||||
|
||||
// Generic error - normalize to avoid high cardinality
|
||||
return normalizeGenericError(err.Error())
|
||||
}
|
||||
|
||||
// normalizeNetworkError normalizes network error messages to prevent high cardinality
|
||||
// by removing variable data like port numbers and connection details
|
||||
func normalizeNetworkError(err error) string {
|
||||
if err == nil {
|
||||
return "network:unknown"
|
||||
}
|
||||
|
||||
errStr := strings.ToLower(err.Error())
|
||||
|
||||
// I/O timeout errors
|
||||
if strings.Contains(errStr, "i/o timeout") {
|
||||
return "network:io_timeout"
|
||||
}
|
||||
|
||||
// Connection refused errors
|
||||
if strings.Contains(errStr, "connection refused") {
|
||||
return "network:connection_refused"
|
||||
}
|
||||
|
||||
// Connection reset errors
|
||||
if strings.Contains(errStr, "connection reset") {
|
||||
return "network:connection_reset"
|
||||
}
|
||||
|
||||
// EOF errors
|
||||
if strings.Contains(errStr, "eof") {
|
||||
return "network:eof"
|
||||
}
|
||||
|
||||
// Broken pipe
|
||||
if strings.Contains(errStr, "broken pipe") {
|
||||
return "network:broken_pipe"
|
||||
}
|
||||
|
||||
// Connection closed
|
||||
if strings.Contains(errStr, "use of closed") || strings.Contains(errStr, "connection closed") {
|
||||
return "network:connection_closed"
|
||||
}
|
||||
|
||||
// DNS/hostname errors
|
||||
if strings.Contains(errStr, "no such host") || strings.Contains(errStr, "lookup") {
|
||||
return "network:dns_error"
|
||||
}
|
||||
|
||||
// Default to generic network error
|
||||
return "network:other"
|
||||
}
|
||||
|
||||
// normalizeGenericError normalizes generic error messages to prevent high cardinality
|
||||
func normalizeGenericError(errStr string) string {
|
||||
// Remove common variable patterns
|
||||
// Port numbers, IP addresses, etc. would create high cardinality
|
||||
errLower := strings.ToLower(errStr)
|
||||
|
||||
// Context deadline exceeded
|
||||
if strings.Contains(errLower, "context deadline exceeded") {
|
||||
return "context_deadline_exceeded"
|
||||
}
|
||||
|
||||
// Context canceled
|
||||
if strings.Contains(errLower, "context canceled") {
|
||||
return "context_canceled"
|
||||
}
|
||||
|
||||
// Pool errors
|
||||
if strings.Contains(errLower, "pool") {
|
||||
if strings.Contains(errLower, "timeout") {
|
||||
return "pool_timeout"
|
||||
}
|
||||
if strings.Contains(errLower, "closed") {
|
||||
return "pool_closed"
|
||||
}
|
||||
return "pool_error"
|
||||
}
|
||||
|
||||
// Return first word if it looks like a category, otherwise return "other"
|
||||
parts := strings.SplitN(errStr, " ", 2)
|
||||
if len(parts) > 0 && len(parts[0]) > 0 && len(parts[0]) <= 30 {
|
||||
// Only use the first word if it doesn't contain variable data
|
||||
firstWord := strings.TrimSuffix(strings.TrimSuffix(parts[0], ":"), ",")
|
||||
if !strings.ContainsAny(firstWord, "0123456789./[]") {
|
||||
return strings.ToLower(firstWord)
|
||||
}
|
||||
}
|
||||
|
||||
return "other"
|
||||
}
|
||||
|
||||
// extractRedisErrorPrefix extracts the Redis error prefix (e.g., "ERR", "WRONGTYPE")
|
||||
// Redis errors typically start with an uppercase prefix followed by a space
|
||||
func extractRedisErrorPrefix(err error) string {
|
||||
if err == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
errStr := err.Error()
|
||||
|
||||
// Redis errors typically start with an uppercase prefix
|
||||
// Examples: "ERR ...", "WRONGTYPE ...", "CLUSTERDOWN ..."
|
||||
parts := strings.SplitN(errStr, " ", 2)
|
||||
if len(parts) > 0 {
|
||||
prefix := parts[0]
|
||||
// Check if it's all uppercase (Redis error convention)
|
||||
if prefix == strings.ToUpper(prefix) && len(prefix) > 0 {
|
||||
return prefix
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// isNetworkError checks if an error is a network-related error
|
||||
func isNetworkError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check for net.Error interface (standard way to detect network errors)
|
||||
_, ok := err.(net.Error)
|
||||
return ok
|
||||
}
|
||||
|
||||
// isTimeoutError checks if an error is a timeout error
|
||||
func isTimeoutError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check for timeoutError interface (works with wrapped errors)
|
||||
var te timeoutError
|
||||
if errors.As(err, &te) {
|
||||
return te.Timeout()
|
||||
}
|
||||
|
||||
// Check for net.Error specifically (common case for network timeouts)
|
||||
var netErr net.Error
|
||||
if errors.As(err, &netErr) {
|
||||
return netErr.Timeout()
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// getErrorCategory returns the error category from an error.
|
||||
// It checks for TLS, auth, network, and server errors.
|
||||
func getErrorCategory(err error) string {
|
||||
if err == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
errStr := err.Error()
|
||||
|
||||
// For actual errors, also check error types
|
||||
if isNetworkError(err) || isTimeoutError(err) {
|
||||
return "network"
|
||||
}
|
||||
|
||||
// Check for Redis server errors by prefix
|
||||
if prefix := extractRedisErrorPrefix(err); prefix != "" {
|
||||
return "server"
|
||||
}
|
||||
|
||||
return getErrorCategoryFromString(errStr)
|
||||
}
|
||||
|
||||
// getErrorCategoryFromString returns the error category from an error string or error type.
|
||||
// This is used both by getErrorCategory (for errors) and directly for error type strings.
|
||||
func getErrorCategoryFromString(errStr string) string {
|
||||
if errStr == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
errLower := strings.ToLower(errStr)
|
||||
|
||||
// Check for TLS errors
|
||||
if strings.Contains(errLower, "tls") || strings.Contains(errLower, "certificate") ||
|
||||
strings.Contains(errLower, "x509") || strings.Contains(errLower, "ssl") {
|
||||
return "tls"
|
||||
}
|
||||
|
||||
// Check for auth errors
|
||||
if strings.Contains(errLower, "auth") || strings.Contains(errLower, "noauth") ||
|
||||
strings.Contains(errLower, "wrongpass") || strings.Contains(errLower, "noperm") ||
|
||||
strings.Contains(errLower, "permission") {
|
||||
return "auth"
|
||||
}
|
||||
|
||||
// Check for network errors (transport/DNS/socket issues)
|
||||
if strings.Contains(errLower, "network") || strings.Contains(errLower, "timeout") ||
|
||||
strings.Contains(errLower, "connection refused") ||
|
||||
strings.Contains(errLower, "connection reset") ||
|
||||
strings.Contains(errLower, "no route to host") ||
|
||||
strings.Contains(errLower, "network is unreachable") ||
|
||||
strings.Contains(errLower, "dns lookup") ||
|
||||
strings.Contains(errLower, "dial") ||
|
||||
strings.Contains(errLower, "eof") ||
|
||||
strings.Contains(errLower, "broken pipe") ||
|
||||
strings.Contains(errLower, "i/o") {
|
||||
return "network"
|
||||
}
|
||||
|
||||
// Check for Redis server errors (including cluster errors)
|
||||
// Common Redis error prefixes: ERR, WRONGTYPE, CLUSTERDOWN, MOVED, ASK, READONLY, etc.
|
||||
serverErrors := []string{"err", "wrongtype", "clusterdown", "moved", "ask", "readonly",
|
||||
"crossslot", "tryagain", "loading", "busy", "noscript", "oom", "execabort", "noquorum", "redis:"}
|
||||
for _, prefix := range serverErrors {
|
||||
if strings.Contains(errLower, prefix) {
|
||||
return "server"
|
||||
}
|
||||
}
|
||||
|
||||
// If it looks like an uppercase Redis error prefix, it's a server error
|
||||
if len(errStr) > 0 && errStr == strings.ToUpper(errStr) {
|
||||
return "server"
|
||||
}
|
||||
|
||||
// Uncategorized errors
|
||||
return "other"
|
||||
}
|
||||
|
||||
// splitHostPort splits a host:port string into host and port
|
||||
// This is a simplified version that handles the common cases
|
||||
func splitHostPort(addr string) (host, port string) {
|
||||
// Handle Unix sockets
|
||||
if strings.HasPrefix(addr, "/") || strings.HasPrefix(addr, "@") {
|
||||
return addr, ""
|
||||
}
|
||||
|
||||
host, port, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
// If split fails, return the whole address as host
|
||||
return addr, ""
|
||||
}
|
||||
|
||||
return host, port
|
||||
}
|
||||
|
||||
// parseAddr parses a Redis address into host and port
|
||||
func parseAddr(addr string) (host, port string) {
|
||||
// Handle Unix sockets
|
||||
if strings.HasPrefix(addr, "/") || strings.HasPrefix(addr, "unix://") {
|
||||
return addr, ""
|
||||
}
|
||||
|
||||
// Remove protocol prefix if present
|
||||
addr = strings.TrimPrefix(addr, "redis://")
|
||||
addr = strings.TrimPrefix(addr, "rediss://")
|
||||
|
||||
host, port, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
// No port specified, use default
|
||||
return addr, "6379"
|
||||
}
|
||||
|
||||
return host, port
|
||||
}
|
||||
|
||||
// extractServerInfo extracts server address and port from connection info
|
||||
// For client connections, this is the remote endpoint (server address)
|
||||
func extractServerInfo(cn redis.ConnInfo) (addr, port string) {
|
||||
if cn == nil {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
remoteAddr := cn.RemoteAddr()
|
||||
if remoteAddr == nil {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
addrStr := remoteAddr.String()
|
||||
host, portStr := parseAddr(addrStr)
|
||||
return host, portStr
|
||||
}
|
||||
|
||||
// RecordConnectionCreateTime records the time it took to create a new connection
|
||||
func (r *metricsRecorder) RecordConnectionCreateTime(
|
||||
ctx context.Context,
|
||||
duration time.Duration,
|
||||
cn redis.ConnInfo,
|
||||
) {
|
||||
if r.connectionCreateTime == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Convert duration to seconds (OTel convention)
|
||||
durationSeconds := duration.Seconds()
|
||||
|
||||
// Build attributes
|
||||
attrs := []attribute.KeyValue{
|
||||
attribute.String(AttrDBSystemName, DBSystemRedis),
|
||||
getLibraryVersionAttr(),
|
||||
}
|
||||
|
||||
// Use pool name from connection (set when connection was created)
|
||||
// This ensures consistency with gauge metrics which also use the registered pool name
|
||||
if cn != nil {
|
||||
poolName := cn.PoolName()
|
||||
if poolName != "" {
|
||||
attrs = append(attrs, attribute.String(AttrDBClientConnectionPoolName, poolName))
|
||||
}
|
||||
}
|
||||
|
||||
// Record the histogram
|
||||
r.connectionCreateTime.Record(ctx, durationSeconds, metric.WithAttributes(attrs...))
|
||||
}
|
||||
|
||||
// RecordConnectionRelaxedTimeout records when connection timeout is relaxed/unrelaxed
|
||||
func (r *metricsRecorder) RecordConnectionRelaxedTimeout(
|
||||
ctx context.Context,
|
||||
delta int,
|
||||
cn redis.ConnInfo,
|
||||
poolName, notificationType string,
|
||||
) {
|
||||
if r.connectionRelaxedTimeout == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Build attributes
|
||||
attrs := []attribute.KeyValue{
|
||||
attribute.String(AttrDBSystemName, DBSystemRedis),
|
||||
getLibraryVersionAttr(),
|
||||
}
|
||||
|
||||
// Use pool name from connection (set when connection was created)
|
||||
if cn != nil {
|
||||
connPoolName := cn.PoolName()
|
||||
if connPoolName != "" {
|
||||
attrs = append(attrs, attribute.String(AttrDBClientConnectionPoolName, connPoolName))
|
||||
}
|
||||
}
|
||||
|
||||
// Add notification type
|
||||
attrs = append(attrs, attribute.String(AttrRedisClientConnectionNotification, notificationType))
|
||||
|
||||
// Record the counter (delta can be +1 or -1)
|
||||
r.connectionRelaxedTimeout.Add(ctx, int64(delta), metric.WithAttributes(attrs...))
|
||||
}
|
||||
|
||||
// RecordConnectionHandoff records when a connection is handed off to another node
|
||||
func (r *metricsRecorder) RecordConnectionHandoff(
|
||||
ctx context.Context,
|
||||
cn redis.ConnInfo,
|
||||
poolName string,
|
||||
) {
|
||||
if r.connectionHandoff == nil {
|
||||
return
|
||||
}
|
||||
|
||||
attrs := []attribute.KeyValue{
|
||||
attribute.String(AttrDBSystemName, DBSystemRedis),
|
||||
getLibraryVersionAttr(),
|
||||
}
|
||||
|
||||
// Use pool name from connection (set when connection was created)
|
||||
if cn != nil {
|
||||
connPoolName := cn.PoolName()
|
||||
if connPoolName != "" {
|
||||
attrs = append(attrs, attribute.String(AttrDBClientConnectionPoolName, connPoolName))
|
||||
}
|
||||
}
|
||||
|
||||
// Record the counter
|
||||
r.connectionHandoff.Add(ctx, 1, metric.WithAttributes(attrs...))
|
||||
}
|
||||
|
||||
// RecordError records client errors (ASK, MOVED, handshake failures, etc.)
|
||||
func (r *metricsRecorder) RecordError(
|
||||
ctx context.Context,
|
||||
errorType string,
|
||||
cn redis.ConnInfo,
|
||||
statusCode string,
|
||||
isInternal bool,
|
||||
retryAttempts int,
|
||||
) {
|
||||
if r.clientErrors == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Extract server address and peer address from connection (may be nil for some errors)
|
||||
// For client connections, peer address is the same as server address (remote endpoint)
|
||||
var serverAddr, serverPort, peerAddr, peerPort string
|
||||
if cn != nil {
|
||||
serverAddr, serverPort = extractServerInfo(cn)
|
||||
peerAddr, peerPort = serverAddr, serverPort // Peer is same as server for client connections
|
||||
}
|
||||
|
||||
// Build attributes
|
||||
attrs := []attribute.KeyValue{
|
||||
attribute.String(AttrDBSystemName, DBSystemRedis),
|
||||
attribute.String(AttrErrorType, errorType),
|
||||
attribute.String(AttrRedisClientErrorsCategory, getErrorCategoryFromString(errorType)),
|
||||
attribute.String(AttrDBResponseStatusCode, statusCode),
|
||||
attribute.Bool(AttrRedisClientErrorsInternal, isInternal),
|
||||
attribute.Int(AttrRedisClientOperationRetryAttempts, retryAttempts),
|
||||
getLibraryVersionAttr(),
|
||||
}
|
||||
|
||||
// Add server info if available
|
||||
if serverAddr != "" {
|
||||
attrs = append(attrs, attribute.String(AttrServerAddress, serverAddr))
|
||||
attrs = addServerPortIfNonDefault(attrs, serverPort)
|
||||
}
|
||||
|
||||
// Add peer info if available
|
||||
if peerAddr != "" {
|
||||
attrs = append(attrs, attribute.String(AttrNetworkPeerAddress, peerAddr))
|
||||
if peerPort != "" {
|
||||
attrs = append(attrs, attribute.String(AttrNetworkPeerPort, peerPort))
|
||||
}
|
||||
}
|
||||
|
||||
// Record the counter
|
||||
r.clientErrors.Add(ctx, 1, metric.WithAttributes(attrs...))
|
||||
}
|
||||
|
||||
// RecordMaintenanceNotification records when a maintenance notification is received
|
||||
func (r *metricsRecorder) RecordMaintenanceNotification(
|
||||
ctx context.Context,
|
||||
cn redis.ConnInfo,
|
||||
notificationType string,
|
||||
) {
|
||||
if r.maintenanceNotifications == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Extract server address and peer address from connection
|
||||
// For client connections, peer address is the same as server address (remote endpoint)
|
||||
serverAddr, serverPort := extractServerInfo(cn)
|
||||
peerAddr, peerPort := serverAddr, serverPort // Peer is same as server for client connections
|
||||
|
||||
// Build attributes
|
||||
attrs := []attribute.KeyValue{
|
||||
attribute.String(AttrDBSystemName, DBSystemRedis),
|
||||
attribute.String(AttrServerAddress, serverAddr),
|
||||
getLibraryVersionAttr(),
|
||||
attribute.String(AttrRedisClientConnectionNotification, notificationType),
|
||||
}
|
||||
|
||||
// Add server.port if not default
|
||||
attrs = addServerPortIfNonDefault(attrs, serverPort)
|
||||
|
||||
// Add peer info if available
|
||||
if peerAddr != "" {
|
||||
attrs = append(attrs, attribute.String(AttrNetworkPeerAddress, peerAddr))
|
||||
if peerPort != "" {
|
||||
attrs = append(attrs, attribute.String(AttrNetworkPeerPort, peerPort))
|
||||
}
|
||||
}
|
||||
|
||||
// Record the counter
|
||||
r.maintenanceNotifications.Add(ctx, 1, metric.WithAttributes(attrs...))
|
||||
}
|
||||
|
||||
// RecordConnectionWaitTime records db.client.connection.wait_time metric
|
||||
func (r *metricsRecorder) RecordConnectionWaitTime(
|
||||
ctx context.Context,
|
||||
duration time.Duration,
|
||||
cn redis.ConnInfo,
|
||||
) {
|
||||
if r.connectionWaitTime == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Build attributes
|
||||
attrs := []attribute.KeyValue{
|
||||
attribute.String(AttrDBSystemName, DBSystemRedis),
|
||||
getLibraryVersionAttr(),
|
||||
}
|
||||
|
||||
// Use pool name from connection (set when connection was created)
|
||||
if cn != nil {
|
||||
poolName := cn.PoolName()
|
||||
if poolName != "" {
|
||||
attrs = append(attrs, attribute.String(AttrDBClientConnectionPoolName, poolName))
|
||||
}
|
||||
}
|
||||
|
||||
// Record the histogram (duration in seconds)
|
||||
r.connectionWaitTime.Record(ctx, duration.Seconds(), metric.WithAttributes(attrs...))
|
||||
}
|
||||
|
||||
// RecordConnectionClosed records redis.client.connection.closed metric
|
||||
func (r *metricsRecorder) RecordConnectionClosed(
|
||||
ctx context.Context,
|
||||
cn redis.ConnInfo,
|
||||
reason string,
|
||||
err error,
|
||||
) {
|
||||
if r.connectionClosed == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Build attributes
|
||||
attrs := []attribute.KeyValue{
|
||||
attribute.String(AttrDBSystemName, DBSystemRedis),
|
||||
getLibraryVersionAttr(),
|
||||
}
|
||||
|
||||
// Use pool name from connection (set when connection was created)
|
||||
if cn != nil {
|
||||
poolName := cn.PoolName()
|
||||
if poolName != "" {
|
||||
attrs = append(attrs, attribute.String(AttrDBClientConnectionPoolName, poolName))
|
||||
}
|
||||
}
|
||||
|
||||
// Add error type and category (always required per spec)
|
||||
// Use classifyError to normalize error messages and prevent high cardinality
|
||||
if err != nil {
|
||||
// Normalize the close reason to prevent high cardinality from variable data
|
||||
// (e.g., port numbers, connection IDs in error messages)
|
||||
normalizedReason := classifyError(err)
|
||||
attrs = append(attrs, attribute.String(AttrRedisClientConnectionCloseReason, normalizedReason))
|
||||
attrs = append(attrs, attribute.String(AttrErrorType, normalizedReason))
|
||||
attrs = append(attrs, attribute.String(AttrRedisClientErrorsCategory, getErrorCategory(err)))
|
||||
} else {
|
||||
// For non-error closures, use reason directly (these are controlled strings like "pool_closed")
|
||||
attrs = append(attrs, attribute.String(AttrRedisClientConnectionCloseReason, reason))
|
||||
attrs = append(attrs, attribute.String(AttrErrorType, reason))
|
||||
attrs = append(attrs, attribute.String(AttrRedisClientErrorsCategory, getErrorCategoryFromString(reason)))
|
||||
}
|
||||
|
||||
// Record the counter
|
||||
r.connectionClosed.Add(ctx, 1, metric.WithAttributes(attrs...))
|
||||
}
|
||||
|
||||
// RecordPubSubMessage records redis.client.pubsub.messages metric
|
||||
func (r *metricsRecorder) RecordPubSubMessage(
|
||||
ctx context.Context,
|
||||
cn redis.ConnInfo,
|
||||
direction string,
|
||||
channel string,
|
||||
sharded bool,
|
||||
) {
|
||||
if r.pubsubMessages == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Extract server address and peer address from connection
|
||||
serverAddr, serverPort := extractServerInfo(cn)
|
||||
peerAddr, peerPort := serverAddr, serverPort
|
||||
|
||||
// Build attributes
|
||||
attrs := []attribute.KeyValue{
|
||||
attribute.String(AttrDBSystemName, DBSystemRedis),
|
||||
attribute.String(AttrServerAddress, serverAddr),
|
||||
attribute.String(AttrRedisClientPubSubDirection, direction), // "sent" or "received"
|
||||
attribute.Bool(AttrRedisClientPubSubSharded, sharded),
|
||||
getLibraryVersionAttr(),
|
||||
}
|
||||
|
||||
// Add channel name if not hidden for cardinality reduction
|
||||
if !r.cfg.hidePubSubChannelNames && channel != "" {
|
||||
attrs = append(attrs, attribute.String(AttrRedisClientPubSubChannel, channel))
|
||||
}
|
||||
|
||||
// Add server.port if not default
|
||||
attrs = addServerPortIfNonDefault(attrs, serverPort)
|
||||
|
||||
// Add peer info
|
||||
if peerAddr != "" {
|
||||
attrs = append(attrs, attribute.String(AttrNetworkPeerAddress, peerAddr))
|
||||
if peerPort != "" {
|
||||
attrs = append(attrs, attribute.String(AttrNetworkPeerPort, peerPort))
|
||||
}
|
||||
}
|
||||
|
||||
// Record the counter
|
||||
r.pubsubMessages.Add(ctx, 1, metric.WithAttributes(attrs...))
|
||||
}
|
||||
|
||||
// RecordStreamLag records redis.client.stream.lag metric
|
||||
func (r *metricsRecorder) RecordStreamLag(
|
||||
ctx context.Context,
|
||||
lag time.Duration,
|
||||
cn redis.ConnInfo,
|
||||
streamName string,
|
||||
consumerGroup string,
|
||||
consumerName string,
|
||||
) {
|
||||
if r.streamLag == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Extract server address and peer address from connection
|
||||
serverAddr, serverPort := extractServerInfo(cn)
|
||||
peerAddr, peerPort := serverAddr, serverPort
|
||||
|
||||
// Build attributes
|
||||
attrs := []attribute.KeyValue{
|
||||
attribute.String(AttrDBSystemName, DBSystemRedis),
|
||||
attribute.String(AttrServerAddress, serverAddr),
|
||||
attribute.String(AttrRedisClientStreamConsumerGroup, consumerGroup),
|
||||
attribute.String(AttrRedisClientStreamConsumerName, consumerName),
|
||||
getLibraryVersionAttr(),
|
||||
}
|
||||
|
||||
// Add stream name if not hidden for cardinality reduction
|
||||
if !r.cfg.hideStreamNames && streamName != "" {
|
||||
attrs = append(attrs, attribute.String(AttrRedisClientStreamName, streamName))
|
||||
}
|
||||
|
||||
// Add server.port if not default
|
||||
attrs = addServerPortIfNonDefault(attrs, serverPort)
|
||||
|
||||
// Add peer info
|
||||
if peerAddr != "" {
|
||||
attrs = append(attrs, attribute.String(AttrNetworkPeerAddress, peerAddr))
|
||||
if peerPort != "" {
|
||||
attrs = append(attrs, attribute.String(AttrNetworkPeerPort, peerPort))
|
||||
}
|
||||
}
|
||||
|
||||
// Record the histogram (lag in seconds)
|
||||
r.streamLag.Record(ctx, lag.Seconds(), metric.WithAttributes(attrs...))
|
||||
}
|
||||
|
||||
// RecordConnectionCount records a change in connection count (UpDownCounter).
|
||||
// Per OTel semantic conventions, db.client.connection.count is an UpDownCounter.
|
||||
// delta: +1 when connection added, -1 when connection removed
|
||||
// state: connection state (e.g., "idle", "used")
|
||||
// isPubSub: true if this is a PubSub connection
|
||||
func (r *metricsRecorder) RecordConnectionCount(
|
||||
ctx context.Context,
|
||||
delta int,
|
||||
cn redis.ConnInfo,
|
||||
state string,
|
||||
isPubSub bool,
|
||||
) {
|
||||
if r.connectionCount == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Build attributes
|
||||
attrs := []attribute.KeyValue{
|
||||
attribute.String(AttrDBSystemName, DBSystemRedis),
|
||||
getLibraryVersionAttr(),
|
||||
attribute.String(AttrDBClientConnectionState, state),
|
||||
attribute.Bool(AttrRedisClientConnectionPubSub, isPubSub),
|
||||
}
|
||||
|
||||
// Use pool name from connection (set when connection was created)
|
||||
if cn != nil {
|
||||
poolName := cn.PoolName()
|
||||
if poolName != "" {
|
||||
attrs = append(attrs, attribute.String(AttrDBClientConnectionPoolName, poolName))
|
||||
|
||||
// Extract server info from pool name
|
||||
serverAddr, serverPort, _ := parsePoolName(poolName)
|
||||
if serverAddr != "" {
|
||||
attrs = append(attrs, attribute.String(AttrServerAddress, serverAddr))
|
||||
}
|
||||
attrs = addServerPortIfNonDefault(attrs, serverPort)
|
||||
}
|
||||
}
|
||||
|
||||
// Record the counter (delta can be +1 or -1)
|
||||
r.connectionCount.Add(ctx, int64(delta), metric.WithAttributes(attrs...))
|
||||
}
|
||||
|
||||
// RecordPendingRequests records a change in pending requests (UpDownCounter).
|
||||
// Per OTel semantic conventions, db.client.connection.pending_requests is an UpDownCounter.
|
||||
// delta: +1 when request starts waiting, -1 when request stops waiting
|
||||
// poolName is passed explicitly because we may not have a connection yet when request starts
|
||||
func (r *metricsRecorder) RecordPendingRequests(
|
||||
ctx context.Context,
|
||||
delta int,
|
||||
cn redis.ConnInfo,
|
||||
poolName string,
|
||||
) {
|
||||
if r.connectionPendingReqs == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Build attributes
|
||||
attrs := []attribute.KeyValue{
|
||||
attribute.String(AttrDBSystemName, DBSystemRedis),
|
||||
getLibraryVersionAttr(),
|
||||
}
|
||||
|
||||
// Use explicit pool name (preferred) or fall back to connection's pool name
|
||||
effectivePoolName := poolName
|
||||
if effectivePoolName == "" && cn != nil {
|
||||
effectivePoolName = cn.PoolName()
|
||||
}
|
||||
|
||||
if effectivePoolName != "" {
|
||||
attrs = append(attrs, attribute.String(AttrDBClientConnectionPoolName, effectivePoolName))
|
||||
|
||||
// Extract server info from pool name
|
||||
serverAddr, serverPort, _ := parsePoolName(effectivePoolName)
|
||||
if serverAddr != "" {
|
||||
attrs = append(attrs, attribute.String(AttrServerAddress, serverAddr))
|
||||
}
|
||||
attrs = addServerPortIfNonDefault(attrs, serverPort)
|
||||
}
|
||||
|
||||
// Record the counter (delta can be +1 or -1)
|
||||
r.connectionPendingReqs.Add(ctx, int64(delta), metric.WithAttributes(attrs...))
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package redisotel
|
||||
|
||||
import "testing"
|
||||
|
||||
// Expected metric names per OTel semantic conventions.
|
||||
// Reference: https://opentelemetry.io/docs/specs/semconv/database/database-metrics/
|
||||
const (
|
||||
semconvOperationDuration = "db.client.operation.duration"
|
||||
semconvConnectionCount = "db.client.connection.count"
|
||||
semconvConnectionCreateTime = "db.client.connection.create_time"
|
||||
semconvConnectionWaitTime = "db.client.connection.wait_time"
|
||||
semconvConnectionPending = "db.client.connection.pending_requests"
|
||||
)
|
||||
|
||||
// TestMetricDefinitionsMatchSemconv verifies metric names match OTel semantic conventions.
|
||||
func TestMetricDefinitionsMatchSemconv(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
got string
|
||||
expected string
|
||||
}{
|
||||
{"db.client.operation.duration", MetricOperationDuration, semconvOperationDuration},
|
||||
{"db.client.connection.count", MetricConnectionCount, semconvConnectionCount},
|
||||
{"db.client.connection.create_time", MetricConnectionCreateTime, semconvConnectionCreateTime},
|
||||
{"db.client.connection.wait_time", MetricConnectionWaitTime, semconvConnectionWaitTime},
|
||||
{"db.client.connection.pending_requests", MetricConnectionPendingReqs, semconvConnectionPending},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.got != tt.expected {
|
||||
t.Errorf("got %q, want %q", tt.got, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestSemconvMetricTypes documents expected metric instrument types.
|
||||
// Semconv specifies UpDownCounter for connection.count and pending_requests,
|
||||
// but this implementation uses ObservableGauge (known deviation, see issue #3730).
|
||||
func TestSemconvMetricTypes(t *testing.T) {
|
||||
t.Run("connection.count uses Gauge (semconv specifies UpDownCounter)", func(t *testing.T) {
|
||||
// Known deviation: using ObservableGauge instead of UpDownCounter
|
||||
})
|
||||
|
||||
t.Run("pending_requests uses Gauge (semconv specifies UpDownCounter)", func(t *testing.T) {
|
||||
// Known deviation: using ObservableGauge instead of UpDownCounter
|
||||
})
|
||||
|
||||
t.Run("operation.duration uses Histogram (correct)", func(t *testing.T) {
|
||||
// Matches semconv: Float64Histogram
|
||||
})
|
||||
|
||||
t.Run("connection.create_time uses Histogram (correct)", func(t *testing.T) {
|
||||
// Matches semconv: Float64Histogram
|
||||
})
|
||||
|
||||
t.Run("connection.wait_time uses Histogram (correct)", func(t *testing.T) {
|
||||
// Matches semconv: Float64Histogram
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
package redisotel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/sdk/metric"
|
||||
"go.opentelemetry.io/otel/sdk/metric/metricdata"
|
||||
)
|
||||
|
||||
const (
|
||||
stressTestDuration = 30 * time.Second
|
||||
stressTestConcurrency = 50
|
||||
stressTestMinDelay = 10 * time.Millisecond
|
||||
stressTestMaxDelay = 100 * time.Millisecond
|
||||
stressTestStatusInterval = 5 * time.Second
|
||||
)
|
||||
|
||||
// TestMetricsUnderStress validates metrics recording under concurrent load.
|
||||
func TestMetricsUnderStress(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
testClient := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
|
||||
if err := testClient.Ping(ctx).Err(); err != nil {
|
||||
testClient.Close()
|
||||
t.Skip("Redis not available at localhost:6379")
|
||||
}
|
||||
testClient.Close()
|
||||
|
||||
reader := metric.NewManualReader()
|
||||
meterProvider := metric.NewMeterProvider(metric.WithReader(reader))
|
||||
defer func() {
|
||||
_ = meterProvider.Shutdown(ctx)
|
||||
}()
|
||||
|
||||
otel.SetMeterProvider(meterProvider)
|
||||
resetObservabilityForTest()
|
||||
|
||||
otelInstance := GetObservabilityInstance()
|
||||
config := NewConfig().
|
||||
WithEnabled(true).
|
||||
WithMeterProvider(meterProvider).
|
||||
WithMetricGroups(MetricGroupAll)
|
||||
|
||||
if err := otelInstance.Init(config); err != nil {
|
||||
t.Fatalf("Failed to initialize OTel: %v", err)
|
||||
}
|
||||
defer otelInstance.Shutdown()
|
||||
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6379",
|
||||
PoolSize: stressTestConcurrency,
|
||||
MinIdleConns: 10,
|
||||
})
|
||||
defer rdb.Close()
|
||||
|
||||
var opsCompleted atomic.Int64
|
||||
var opsErrors atomic.Int64
|
||||
startTime := time.Now()
|
||||
deadline := startTime.Add(stressTestDuration)
|
||||
|
||||
statusTicker := time.NewTicker(stressTestStatusInterval)
|
||||
defer statusTicker.Stop()
|
||||
done := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-statusTicker.C:
|
||||
elapsed := time.Since(startTime).Seconds()
|
||||
ops := opsCompleted.Load()
|
||||
t.Logf("[%.0fs] %d ops, %.1f ops/sec", elapsed, ops, float64(ops)/elapsed)
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < stressTestConcurrency; i++ {
|
||||
wg.Add(1)
|
||||
go func(workerID int) {
|
||||
defer wg.Done()
|
||||
for time.Now().Before(deadline) {
|
||||
key := fmt.Sprintf("stress_test_key_%d_%d", workerID, rand.Int63())
|
||||
value := fmt.Sprintf("value_%d", time.Now().UnixNano())
|
||||
|
||||
if err := rdb.Set(ctx, key, value, time.Minute).Err(); err != nil {
|
||||
opsErrors.Add(1)
|
||||
} else {
|
||||
opsCompleted.Add(1)
|
||||
}
|
||||
|
||||
if _, err := rdb.Get(ctx, key).Result(); err != nil && err != redis.Nil {
|
||||
opsErrors.Add(1)
|
||||
} else {
|
||||
opsCompleted.Add(1)
|
||||
}
|
||||
|
||||
delay := stressTestMinDelay + time.Duration(rand.Int63n(int64(stressTestMaxDelay-stressTestMinDelay)))
|
||||
time.Sleep(delay)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(done)
|
||||
|
||||
totalOps := opsCompleted.Load()
|
||||
totalErrors := opsErrors.Load()
|
||||
elapsed := time.Since(startTime)
|
||||
t.Logf("Completed: %d ops in %v (%.1f ops/sec), %d errors",
|
||||
totalOps, elapsed.Round(time.Second), float64(totalOps)/elapsed.Seconds(), totalErrors)
|
||||
|
||||
var rm metricdata.ResourceMetrics
|
||||
if err := reader.Collect(ctx, &rm); err != nil {
|
||||
t.Fatalf("Failed to collect metrics: %v", err)
|
||||
}
|
||||
|
||||
validateMetrics(t, rm)
|
||||
}
|
||||
|
||||
func validateMetrics(t *testing.T, rm metricdata.ResourceMetrics) {
|
||||
metricsFound := make(map[string]bool)
|
||||
for _, sm := range rm.ScopeMetrics {
|
||||
for _, m := range sm.Metrics {
|
||||
metricsFound[m.Name] = true
|
||||
}
|
||||
}
|
||||
|
||||
required := []string{
|
||||
MetricConnectionCount,
|
||||
MetricConnectionCreateTime,
|
||||
MetricOperationDuration,
|
||||
}
|
||||
|
||||
for _, name := range required {
|
||||
if !metricsFound[name] {
|
||||
t.Errorf("Required metric not found: %s", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func resetObservabilityForTest() {
|
||||
observabilityInstanceOnce = sync.Once{}
|
||||
observabilityInstance = nil
|
||||
}
|
||||
|
||||
// TestTracingAndMetricsCompatibility verifies that redisotel (tracing) and
|
||||
// redisotel-native (metrics) can be used together without conflicts.
|
||||
// Tracing uses AddHook (per-client), metrics uses SetOTelRecorder (global).
|
||||
func TestTracingAndMetricsCompatibility(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
testClient := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
|
||||
if err := testClient.Ping(ctx).Err(); err != nil {
|
||||
testClient.Close()
|
||||
t.Skip("Redis not available at localhost:6379")
|
||||
}
|
||||
testClient.Close()
|
||||
|
||||
reader := metric.NewManualReader()
|
||||
meterProvider := metric.NewMeterProvider(metric.WithReader(reader))
|
||||
defer meterProvider.Shutdown(ctx)
|
||||
|
||||
otel.SetMeterProvider(meterProvider)
|
||||
resetObservabilityForTest()
|
||||
|
||||
otelInstance := GetObservabilityInstance()
|
||||
config := NewConfig().
|
||||
WithEnabled(true).
|
||||
WithMeterProvider(meterProvider).
|
||||
WithMetricGroups(MetricGroupAll)
|
||||
|
||||
if err := otelInstance.Init(config); err != nil {
|
||||
t.Fatalf("Failed to initialize OTel metrics: %v", err)
|
||||
}
|
||||
defer otelInstance.Shutdown()
|
||||
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6379",
|
||||
PoolSize: 5,
|
||||
})
|
||||
defer rdb.Close()
|
||||
|
||||
// In production, also call: redisotel.InstrumentTracing(rdb)
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
key := fmt.Sprintf("compat-test-%d", i)
|
||||
if err := rdb.Set(ctx, key, "value", time.Minute).Err(); err != nil {
|
||||
t.Fatalf("SET failed: %v", err)
|
||||
}
|
||||
if _, err := rdb.Get(ctx, key).Result(); err != nil {
|
||||
t.Fatalf("GET failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
var rm metricdata.ResourceMetrics
|
||||
if err := reader.Collect(ctx, &rm); err != nil {
|
||||
t.Fatalf("Failed to collect metrics: %v", err)
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, sm := range rm.ScopeMetrics {
|
||||
for _, m := range sm.Metrics {
|
||||
if m.Name == MetricOperationDuration {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
t.Error("Expected to find db.client.operation.duration metric")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,377 @@
|
||||
package redisotel
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
"go.opentelemetry.io/otel/semconv/v1.38.0/dbconv"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// Metric name constants
|
||||
const (
|
||||
// Operation metrics
|
||||
MetricOperationDuration = "db.client.operation.duration"
|
||||
|
||||
// Connection metrics
|
||||
MetricConnectionCount = "db.client.connection.count"
|
||||
MetricConnectionCreateTime = "db.client.connection.create_time"
|
||||
MetricConnectionWaitTime = "db.client.connection.wait_time"
|
||||
MetricConnectionPendingReqs = "db.client.connection.pending_requests"
|
||||
MetricConnectionRelaxedTimeout = "redis.client.connection.relaxed_timeout"
|
||||
MetricConnectionHandoff = "redis.client.connection.handoff"
|
||||
MetricConnectionClosed = "redis.client.connection.closed"
|
||||
|
||||
// Resiliency metrics
|
||||
MetricClientErrors = "redis.client.errors"
|
||||
MetricMaintenanceNotifications = "redis.client.maintenance.notifications"
|
||||
|
||||
// Pub/Sub metrics
|
||||
MetricPubSubMessages = "redis.client.pubsub.messages"
|
||||
|
||||
// Stream metrics
|
||||
MetricStreamLag = "redis.client.stream.lag"
|
||||
|
||||
// Special pool names
|
||||
PoolNameMain = "main"
|
||||
PoolNamePubSub = "pubsub"
|
||||
)
|
||||
|
||||
var (
|
||||
// Global observability instance
|
||||
observabilityInstance *ObservabilityInstance
|
||||
observabilityInstanceOnce sync.Once
|
||||
)
|
||||
|
||||
// ObservabilityInstance manages the global observability singleton.
|
||||
type ObservabilityInstance struct {
|
||||
mu sync.RWMutex
|
||||
config *Config
|
||||
recorder *metricsRecorder
|
||||
initialized bool
|
||||
}
|
||||
|
||||
// GetObservabilityInstance returns the global observability singleton.
|
||||
func GetObservabilityInstance() *ObservabilityInstance {
|
||||
observabilityInstanceOnce.Do(func() {
|
||||
observabilityInstance = &ObservabilityInstance{}
|
||||
})
|
||||
return observabilityInstance
|
||||
}
|
||||
|
||||
// Init initializes OpenTelemetry observability globally for all Redis clients.
|
||||
// This should be called once at application startup, BEFORE creating any Redis clients.
|
||||
// After initialization, all Redis clients will automatically collect and export
|
||||
// metrics without needing any additional configuration.
|
||||
func (o *ObservabilityInstance) Init(cfg *Config) error {
|
||||
o.mu.Lock()
|
||||
defer o.mu.Unlock()
|
||||
|
||||
// If already initialized, return error
|
||||
if o.initialized {
|
||||
return errors.New("redisotel: already initialized, call Shutdown() before reinitializing")
|
||||
}
|
||||
|
||||
o.config = cfg
|
||||
|
||||
if !cfg.Enabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get meter provider (use global if not provided)
|
||||
meterProvider := cfg.MeterProvider
|
||||
if meterProvider == nil {
|
||||
meterProvider = otel.GetMeterProvider()
|
||||
}
|
||||
|
||||
meter := meterProvider.Meter(
|
||||
"github.com/redis/go-redis",
|
||||
metric.WithInstrumentationVersion(redis.Version()),
|
||||
)
|
||||
|
||||
internalCfg := o.configToInternal(cfg)
|
||||
recorder, err := o.createRecorder(meter, internalCfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create metrics recorder: %w", err)
|
||||
}
|
||||
|
||||
o.recorder = recorder
|
||||
o.initialized = true
|
||||
redis.SetOTelRecorder(recorder)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsEnabled returns true if observability is initialized and enabled.
|
||||
func (o *ObservabilityInstance) IsEnabled() bool {
|
||||
o.mu.RLock()
|
||||
defer o.mu.RUnlock()
|
||||
return o.initialized && o.config != nil && o.config.Enabled
|
||||
}
|
||||
|
||||
// Shutdown cleans up resources and flushes any pending metrics.
|
||||
// This should be called at application shutdown.
|
||||
func (o *ObservabilityInstance) Shutdown() error {
|
||||
o.mu.Lock()
|
||||
defer o.mu.Unlock()
|
||||
return o.shutdownLocked()
|
||||
}
|
||||
|
||||
func (o *ObservabilityInstance) shutdownLocked() error {
|
||||
if !o.initialized {
|
||||
return nil
|
||||
}
|
||||
|
||||
redis.SetOTelRecorder(nil)
|
||||
|
||||
// Note: We don't shutdown the MeterProvider since it's owned by the application
|
||||
// The application should call provider.Shutdown() when appropriate
|
||||
|
||||
o.recorder = nil
|
||||
o.initialized = false
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// configToInternal converts the public Config to internal config format
|
||||
func (o *ObservabilityInstance) configToInternal(cfg *Config) config {
|
||||
enabledGroups := make(map[MetricGroup]bool)
|
||||
if cfg.MetricGroups&MetricGroupFlagCommand != 0 {
|
||||
enabledGroups[MetricGroupCommand] = true
|
||||
}
|
||||
if cfg.MetricGroups&MetricGroupFlagConnectionBasic != 0 {
|
||||
enabledGroups[MetricGroupConnectionBasic] = true
|
||||
}
|
||||
if cfg.MetricGroups&MetricGroupFlagResiliency != 0 {
|
||||
enabledGroups[MetricGroupResiliency] = true
|
||||
}
|
||||
if cfg.MetricGroups&MetricGroupFlagConnectionAdvanced != 0 {
|
||||
enabledGroups[MetricGroupConnectionAdvanced] = true
|
||||
}
|
||||
if cfg.MetricGroups&MetricGroupFlagPubSub != 0 {
|
||||
enabledGroups[MetricGroupPubSub] = true
|
||||
}
|
||||
if cfg.MetricGroups&MetricGroupFlagStream != 0 {
|
||||
enabledGroups[MetricGroupStream] = true
|
||||
}
|
||||
|
||||
return config{
|
||||
meterProvider: cfg.MeterProvider,
|
||||
enabled: cfg.Enabled,
|
||||
enabledMetricGroups: enabledGroups,
|
||||
includeCommands: cfg.IncludeCommands,
|
||||
excludeCommands: cfg.ExcludeCommands,
|
||||
hidePubSubChannelNames: cfg.HidePubSubChannelNames,
|
||||
hideStreamNames: cfg.HideStreamNames,
|
||||
histAggregation: cfg.HistogramAggregation,
|
||||
bucketsOperationDuration: cfg.BucketsOperationDuration,
|
||||
bucketsStreamProcessingDuration: cfg.BucketsStreamLag,
|
||||
bucketsConnectionCreateTime: cfg.BucketsConnectionCreateTime,
|
||||
bucketsConnectionWaitTime: cfg.BucketsConnectionWaitTime,
|
||||
}
|
||||
}
|
||||
|
||||
// createRecorder creates a metricsRecorder with all instruments based on config.
|
||||
func (o *ObservabilityInstance) createRecorder(meter metric.Meter, cfg config) (*metricsRecorder, error) {
|
||||
var err error
|
||||
|
||||
var operationDuration metric.Float64Histogram
|
||||
if cfg.isMetricGroupEnabled(MetricGroupCommand) {
|
||||
var operationDurationOpts []metric.Float64HistogramOption
|
||||
if cfg.histAggregation == HistogramAggregationExplicitBucket {
|
||||
operationDurationOpts = append(operationDurationOpts,
|
||||
metric.WithExplicitBucketBoundaries(cfg.bucketsOperationDuration...),
|
||||
)
|
||||
}
|
||||
var operationDurationConv dbconv.ClientOperationDuration
|
||||
operationDurationConv, err = dbconv.NewClientOperationDuration(meter, operationDurationOpts...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create operation duration histogram: %w", err)
|
||||
}
|
||||
operationDuration = operationDurationConv.Inst()
|
||||
}
|
||||
|
||||
var connectionCount metric.Int64UpDownCounter // OTel semconv: UpDownCounter
|
||||
var connectionCreateTime metric.Float64Histogram
|
||||
var connectionRelaxedTimeout metric.Int64UpDownCounter
|
||||
var connectionHandoff metric.Int64Counter
|
||||
|
||||
if cfg.isMetricGroupEnabled(MetricGroupConnectionBasic) {
|
||||
connectionCount, err = meter.Int64UpDownCounter(
|
||||
dbconv.ClientConnectionCount{}.Name(),
|
||||
metric.WithDescription(dbconv.ClientConnectionCount{}.Description()),
|
||||
metric.WithUnit(dbconv.ClientConnectionCount{}.Unit()),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create connection count metric: %w", err)
|
||||
}
|
||||
|
||||
var connectionCreateTimeOpts []metric.Float64HistogramOption
|
||||
if cfg.histAggregation == HistogramAggregationExplicitBucket {
|
||||
connectionCreateTimeOpts = append(connectionCreateTimeOpts,
|
||||
metric.WithExplicitBucketBoundaries(cfg.bucketsConnectionCreateTime...),
|
||||
)
|
||||
}
|
||||
var connectionCreateTimeConv dbconv.ClientConnectionCreateTime
|
||||
connectionCreateTimeConv, err = dbconv.NewClientConnectionCreateTime(meter, connectionCreateTimeOpts...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create connection create time histogram: %w", err)
|
||||
}
|
||||
connectionCreateTime = connectionCreateTimeConv.Inst()
|
||||
|
||||
connectionRelaxedTimeout, err = meter.Int64UpDownCounter(
|
||||
MetricConnectionRelaxedTimeout,
|
||||
metric.WithDescription("How many times the connection timeout has been increased/decreased (after a server maintenance notification)"),
|
||||
metric.WithUnit("{relaxation}"),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create connection relaxed timeout metric: %w", err)
|
||||
}
|
||||
|
||||
connectionHandoff, err = meter.Int64Counter(
|
||||
MetricConnectionHandoff,
|
||||
metric.WithDescription("Connections that have been handed off to another node (e.g after a MOVING notification)"),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create connection handoff metric: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
var clientErrors metric.Int64Counter
|
||||
var maintenanceNotifications metric.Int64Counter
|
||||
|
||||
if cfg.isMetricGroupEnabled(MetricGroupResiliency) {
|
||||
clientErrors, err = meter.Int64Counter(
|
||||
MetricClientErrors,
|
||||
metric.WithDescription("Number of errors handled by the Redis client"),
|
||||
metric.WithUnit("{error}"),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create client errors metric: %w", err)
|
||||
}
|
||||
|
||||
maintenanceNotifications, err = meter.Int64Counter(
|
||||
MetricMaintenanceNotifications,
|
||||
metric.WithDescription("Number of maintenance notifications received"),
|
||||
metric.WithUnit("{notification}"),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create maintenance notifications metric: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
var connectionWaitTime metric.Float64Histogram
|
||||
var connectionClosed metric.Int64Counter
|
||||
var connectionPendingReqs metric.Int64UpDownCounter // OTel semconv: UpDownCounter
|
||||
|
||||
if cfg.isMetricGroupEnabled(MetricGroupConnectionAdvanced) {
|
||||
var connectionWaitTimeOpts []metric.Float64HistogramOption
|
||||
if cfg.histAggregation == HistogramAggregationExplicitBucket {
|
||||
connectionWaitTimeOpts = append(connectionWaitTimeOpts,
|
||||
metric.WithExplicitBucketBoundaries(cfg.bucketsConnectionWaitTime...),
|
||||
)
|
||||
}
|
||||
var connectionWaitTimeConv dbconv.ClientConnectionWaitTime
|
||||
connectionWaitTimeConv, err = dbconv.NewClientConnectionWaitTime(meter, connectionWaitTimeOpts...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create connection wait time histogram: %w", err)
|
||||
}
|
||||
connectionWaitTime = connectionWaitTimeConv.Inst()
|
||||
|
||||
connectionClosed, err = meter.Int64Counter(
|
||||
MetricConnectionClosed,
|
||||
metric.WithDescription("The number of connections that have been closed"),
|
||||
metric.WithUnit("{connection}"),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create connection closed metric: %w", err)
|
||||
}
|
||||
|
||||
connectionPendingReqs, err = meter.Int64UpDownCounter(
|
||||
dbconv.ClientConnectionPendingRequests{}.Name(),
|
||||
metric.WithDescription(dbconv.ClientConnectionPendingRequests{}.Description()),
|
||||
metric.WithUnit(dbconv.ClientConnectionPendingRequests{}.Unit()),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create connection pending requests metric: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
var pubsubMessages metric.Int64Counter
|
||||
|
||||
if cfg.isMetricGroupEnabled(MetricGroupPubSub) {
|
||||
pubsubMessages, err = meter.Int64Counter(
|
||||
MetricPubSubMessages,
|
||||
metric.WithDescription("The number of Pub/Sub messages sent or received"),
|
||||
metric.WithUnit("{message}"),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create Pub/Sub messages metric: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
var streamLag metric.Float64Histogram
|
||||
|
||||
if cfg.isMetricGroupEnabled(MetricGroupStream) {
|
||||
var streamLagOpts []metric.Float64HistogramOption
|
||||
streamLagOpts = append(streamLagOpts,
|
||||
metric.WithDescription("The lag between message creation and consumption in a stream consumer group"),
|
||||
metric.WithUnit("s"),
|
||||
)
|
||||
if cfg.histAggregation == HistogramAggregationExplicitBucket {
|
||||
streamLagOpts = append(streamLagOpts,
|
||||
metric.WithExplicitBucketBoundaries(cfg.bucketsStreamProcessingDuration...),
|
||||
)
|
||||
}
|
||||
streamLag, err = meter.Float64Histogram(
|
||||
MetricStreamLag,
|
||||
streamLagOpts...,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create stream lag histogram: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create recorder
|
||||
recorder := &metricsRecorder{
|
||||
operationDuration: operationDuration,
|
||||
connectionCount: connectionCount,
|
||||
connectionCreateTime: connectionCreateTime,
|
||||
connectionRelaxedTimeout: connectionRelaxedTimeout,
|
||||
connectionHandoff: connectionHandoff,
|
||||
clientErrors: clientErrors,
|
||||
maintenanceNotifications: maintenanceNotifications,
|
||||
connectionWaitTime: connectionWaitTime,
|
||||
connectionClosed: connectionClosed,
|
||||
connectionPendingReqs: connectionPendingReqs,
|
||||
pubsubMessages: pubsubMessages,
|
||||
streamLag: streamLag,
|
||||
cfg: &cfg,
|
||||
}
|
||||
|
||||
return recorder, nil
|
||||
}
|
||||
|
||||
// parsePoolName extracts server address, port, and database index from a pool name.
|
||||
// Pool name format: "host:port/db" or "host/db" or "host:port" or "host"
|
||||
// Returns: (serverAddr, serverPort, dbIndex)
|
||||
func parsePoolName(poolName string) (string, string, string) {
|
||||
// Handle special pool names
|
||||
if poolName == PoolNameMain || poolName == PoolNamePubSub {
|
||||
return "", "", ""
|
||||
}
|
||||
|
||||
parts := strings.Split(poolName, "/")
|
||||
addrPart := parts[0]
|
||||
dbIndex := ""
|
||||
if len(parts) > 1 {
|
||||
dbIndex = parts[1]
|
||||
}
|
||||
host, port := parseAddr(addrPart)
|
||||
return host, port, dbIndex
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
Copyright (c) 2013 The github.com/redis/go-redis Authors.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -100,6 +100,12 @@ func WithAttributes(attrs ...attribute.KeyValue) Option {
|
||||
})
|
||||
}
|
||||
|
||||
func WithPoolName(poolName string) Option {
|
||||
return option(func(conf *config) {
|
||||
conf.poolName = poolName
|
||||
})
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
type TracingOption interface {
|
||||
|
||||
@@ -17,11 +17,12 @@ require (
|
||||
|
||||
require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/go-logr/logr v1.4.1 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
golang.org/x/sys v0.16.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
)
|
||||
|
||||
retract (
|
||||
|
||||
+33
-16
@@ -1,28 +1,45 @@
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
|
||||
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y=
|
||||
go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI=
|
||||
go.opentelemetry.io/otel/metric v1.22.0 h1:lypMQnGyJYeuYPhOM/bgjbFM6WE44W1/T45er4d8Hhg=
|
||||
go.opentelemetry.io/otel/metric v1.22.0/go.mod h1:evJGjVpZv0mQ5QBRJoBF64yMuOf4xCWdXjK8pzFvliY=
|
||||
go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw=
|
||||
go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc=
|
||||
go.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0=
|
||||
go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
||||
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
||||
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -37,6 +37,25 @@ func InstrumentMetrics(rdb redis.UniversalClient, opts ...MetricsOption) error {
|
||||
metric.WithInstrumentationVersion("semver:"+redis.Version()),
|
||||
)
|
||||
}
|
||||
if conf.poolName == "" {
|
||||
switch rdb := rdb.(type) {
|
||||
case *redis.Client:
|
||||
conf.poolName = rdb.Options().Addr
|
||||
case *redis.ClusterClient:
|
||||
for _, addr := range rdb.Options().Addrs {
|
||||
conf.poolName = addr
|
||||
break
|
||||
}
|
||||
case *redis.Ring:
|
||||
for _, addr := range rdb.Options().Addrs {
|
||||
conf.poolName = addr
|
||||
break
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("redisotel: %T not supported", rdb)
|
||||
}
|
||||
}
|
||||
conf.attrs = append(conf.attrs, attribute.String("pool.name", conf.poolName))
|
||||
|
||||
var state *metricsState
|
||||
if conf.closeChan != nil {
|
||||
@@ -93,12 +112,6 @@ func registerClient(rdb *redis.Client, conf *config, state *metricsState) error
|
||||
}
|
||||
}
|
||||
|
||||
if conf.poolName == "" {
|
||||
opt := rdb.Options()
|
||||
conf.poolName = opt.Addr
|
||||
}
|
||||
conf.attrs = append(conf.attrs, attribute.String("pool.name", conf.poolName))
|
||||
|
||||
registration, err := reportPoolStats(rdb, conf)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
Copyright (c) 2013 The github.com/redis/go-redis Authors.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -12,14 +12,13 @@ require (
|
||||
require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
github.com/prometheus/client_model v0.3.0 // indirect
|
||||
github.com/prometheus/common v0.39.0 // indirect
|
||||
github.com/prometheus/procfs v0.9.0 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
golang.org/x/sys v0.4.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
google.golang.org/protobuf v1.33.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
@@ -14,9 +15,13 @@ github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw=
|
||||
github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y=
|
||||
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
|
||||
@@ -26,11 +31,14 @@ github.com/prometheus/common v0.39.0/go.mod h1:6XBZ7lYdLCbkAVhwRsWTZn+IN5AB9F/NX
|
||||
github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
|
||||
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
|
||||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
||||
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
|
||||
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user