merge: upstream/master (preserve Hanzo brand, 100 commits)

This commit is contained in:
2026-06-02 09:58:21 -07:00
225 changed files with 33671 additions and 1971 deletions
+44 -19
View File
@@ -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
+1
View File
@@ -77,3 +77,4 @@ entraid
MiB
KiB
oldstable
backoff
+10 -6
View File
@@ -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 }}
+2 -2
View File
@@ -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 }}
+1 -1
View File
@@ -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
+69
View File
@@ -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"
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+62
View File
@@ -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
+5 -1
View File
@@ -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
View File
@@ -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/
+41 -6
View File
@@ -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
+45 -14
View File
@@ -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()
+256
View File
@@ -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
View File
@@ -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.
+7
View File
@@ -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
+387
View File
@@ -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
+18
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+447
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+39
View File
@@ -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
View File
@@ -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:
+2 -2
View File
@@ -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>}] []}
}
+2 -2
View File
@@ -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
View File
@@ -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()
+2 -2
View File
@@ -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>}] []}
}
+57 -38
View File
@@ -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:
+28 -16
View File
@@ -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 {
+62
View File
@@ -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() {
+16
View File
@@ -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
View File
@@ -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 {
+105
View File
@@ -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) {
+1 -1
View File
@@ -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
)
+16 -2
View File
@@ -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=
-1
View File
@@ -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
)
+15 -2
View File
@@ -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=
+3 -3
View File
@@ -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 -8
View File
@@ -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 -2
View File
@@ -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=
-1
View File
@@ -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
View File
@@ -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=
-1
View File
@@ -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
)
+10 -2
View File
@@ -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=
-1
View File
@@ -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
View File
@@ -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=
-1
View File
@@ -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
)
+11 -2
View File
@@ -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=
+67
View File
@@ -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/)
+37
View File
@@ -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
)
+67
View File
@@ -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=
+123
View File
@@ -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
View File
@@ -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
View File
@@ -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=
-1
View File
@@ -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
View File
@@ -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=
-1
View File
@@ -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
)
+10 -2
View File
@@ -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=
+32
View File
@@ -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.
+12
View File
@@ -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
)
+20
View File
@@ -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=
+150
View File
@@ -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.
+77
View File
@@ -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.
+12
View File
@@ -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
)
+20
View File
@@ -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=
+116
View File
@@ -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!")
}
+117
View File
@@ -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
+12
View File
@@ -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
)
+20
View File
@@ -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=
+123
View File
@@ -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")
}
+6 -6
View File
@@ -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
View File
@@ -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"},
+24
View File
@@ -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)
}
+25
View File
@@ -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.
-1
View File
@@ -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
)
+8 -2
View File
@@ -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=
+25
View File
@@ -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.
-1
View File
@@ -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
)
+9 -2
View File
@@ -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=
+25
View File
@@ -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.
+67
View File
@@ -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"
)
+267
View File
@@ -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
}
+24
View File
@@ -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
)
+43
View File
@@ -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=
+916
View File
@@ -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")
}
}
+377
View File
@@ -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
}
+25
View File
@@ -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.
+6
View File
@@ -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 {
+4 -3
View File
@@ -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
View File
@@ -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=
+19 -6
View File
@@ -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
+25
View File
@@ -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.
+1 -2
View File
@@ -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
)
+12 -4
View File
@@ -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