├── .circleci
└── config.yml
├── .github
├── release-drafter-config.yml
└── workflows
│ └── release-drafter.yml
├── .gitignore
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── client.go
├── client_test.go
├── common.go
├── common_test.go
├── example_client_test.go
├── go.mod
├── go.sum
├── multiget.go
├── multiget_test.go
├── multirange.go
├── multirange_test.go
├── pool.go
├── pool_test.go
├── range.go
├── range_test.go
├── reply_parser.go
├── reply_parser_test.go
└── run_tests.sh
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | # Golang CircleCI 2.0 configuration file
2 | #
3 | # Check https://circleci.com/docs/2.0/language-go/ for more details
4 | version: 2
5 | jobs:
6 | build: # test with redistimeseries:edge
7 | docker:
8 | - image: circleci/golang:1.13
9 | - image: redislabs/redistimeseries:edge
10 |
11 | working_directory: /go/src/github.com/RedisTimeSeries/redistimeseries-go
12 | steps:
13 | - checkout
14 | - run: make checkfmt
15 | - run: make get
16 | - run:
17 | name: "Validate lint"
18 | command: |
19 | curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s -- -b $(go env GOPATH)/bin v1.29.0
20 | make lint
21 | - run: make test
22 | - run: make coverage
23 | - run: bash <(curl -s https://raw.githubusercontent.com/codecov/codecov-bash/master/codecov) -t ${CODECOV_TOKEN}
24 |
25 | workflows:
26 | version: 2
27 | commit:
28 | jobs:
29 | - build
30 | nightly:
31 | triggers:
32 | - schedule:
33 | cron: "0 0 * * *"
34 | filters:
35 | branches:
36 | only:
37 | - master
38 | jobs:
39 | - build
--------------------------------------------------------------------------------
/.github/release-drafter-config.yml:
--------------------------------------------------------------------------------
1 | name-template: 'Version $NEXT_PATCH_VERSION'
2 | tag-template: 'v$NEXT_PATCH_VERSION'
3 | categories:
4 | - title: 'Features'
5 | labels:
6 | - 'feature'
7 | - 'enhancement'
8 | - title: 'Bug Fixes'
9 | labels:
10 | - 'fix'
11 | - 'bugfix'
12 | - 'bug'
13 | - title: 'Maintenance'
14 | label: 'chore'
15 | change-template: '- $TITLE (#$NUMBER)'
16 | exclude-labels:
17 | - 'skip-changelog'
18 | template: |
19 | ## Changes
20 |
21 | $CHANGES
22 |
--------------------------------------------------------------------------------
/.github/workflows/release-drafter.yml:
--------------------------------------------------------------------------------
1 | name: Release Drafter
2 |
3 | on:
4 | push:
5 | # branches to consider in the event; optional, defaults to all
6 | branches:
7 | - master
8 |
9 | jobs:
10 | update_release_draft:
11 | runs-on: ubuntu-latest
12 | steps:
13 | # Drafts your next Release notes as Pull Requests are merged into "master"
14 | - uses: release-drafter/release-drafter@v5
15 | with:
16 | # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml
17 | config-name: release-drafter-config.yml
18 | env:
19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | *.exe
3 | *.exe~
4 | *.dll
5 | *.so
6 | *.dylib
7 |
8 | # Test binary, build with `go test -c`
9 | *.test
10 |
11 | # Output of the go coverage tool, specifically when used with LiteIDE
12 | *.out
13 | coverage.txt
14 |
15 | .idea
16 | .project
17 |
18 | vendor/
19 |
20 | # OS generated files #
21 | ######################
22 | .DS_Store
23 | .DS_Store?
24 | ._*
25 | .Spotlight-V100
26 | .Trashes
27 | ehthumbs.db
28 | Thumbs.db
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # This dockerfile is used to run unit tests.
2 |
3 | FROM golang:1.20.1
4 |
5 | # install redis
6 | RUN git clone -b 5.0 --depth 1 https://github.com/antirez/redis.git
7 | RUN cd redis && make -j install
8 |
9 | RUN curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
10 |
11 | RUN cd / && git clone https://github.com/RedisLabsModules/redistimeseries.git /redistimeseries
12 | COPY . $GOPATH/src/github.com/RedisLabs/redis-timeseries-go
13 | WORKDIR $GOPATH/src/github.com/RedisLabs/redis-timeseries-go
14 | RUN dep ensure -v
15 |
16 | # install redis-timeseries
17 | RUN cd /redistimeseries && \
18 | git submodule init && \
19 | git submodule update && \
20 | git pull --recurse-submodules && \
21 | cd src && \
22 | make -j all
23 |
24 | CMD redis-server --daemonize yes --loadmodule /redistimeseries/src/redistimeseries.so --requirepass SUPERSECRET && \
25 | sleep 1 && \
26 | go test -coverprofile=coverage.out && \
27 | go tool cover -func=coverage.out
28 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | # Go parameters
2 | GOCMD=GO111MODULE=on go
3 | GOBUILD=$(GOCMD) build
4 | GODOC=GO111MODULE=on godoc
5 | GOINSTALL=$(GOCMD) install
6 | GOCLEAN=$(GOCMD) clean
7 | GOTEST=$(GOCMD) test
8 | GOGET=$(GOCMD) get
9 | GOMOD=$(GOCMD) mod
10 | GOFMT=$(GOCMD) fmt
11 |
12 | .PHONY: all test coverage
13 | all: test coverage
14 |
15 | checkfmt:
16 | @echo 'Checking gofmt';\
17 | bash -c "diff -u <(echo -n) <(gofmt -d .)";\
18 | EXIT_CODE=$$?;\
19 | if [ "$$EXIT_CODE" -ne 0 ]; then \
20 | echo '$@: Go files must be formatted with gofmt'; \
21 | fi && \
22 | exit $$EXIT_CODE
23 |
24 | lint:
25 | @golangci-lint run
26 |
27 | doc:
28 | $(GODOC)
29 |
30 | get:
31 | $(GOGET) -t -v ./...
32 |
33 | fmt:
34 | $(GOFMT) ./...
35 |
36 | test: get fmt
37 | $(GOTEST) -count=1 -race -covermode=atomic ./...
38 |
39 | coverage: get test
40 | $(GOTEST) -race -coverprofile=coverage.txt -covermode=atomic .
41 |
42 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://github.com/RedisTimeSeries/RedisTimeSeries-go)
2 | [](https://circleci.com/gh/RedisTimeSeries/redistimeseries-go)
3 | [](https://github.com/RedisTimeSeries/redistimeseries-go/releases/latest)
4 | [](https://codecov.io/gh/RedisTimeSeries/redistimeseries-go)
5 | [](https://godoc.org/github.com/RedisTimeSeries/redistimeseries-go)
6 | [](https://goreportcard.com/report/github.com/RedisTimeSeries/redistimeseries-go)
7 | [](https://lgtm.com/projects/g/RedisTimeSeries/redistimeseries-go/alerts/)
8 |
9 | # redistimeseries-go
10 | [](https://forum.redislabs.com/c/modules/redistimeseries)
11 | [](https://discord.gg/KExRgMb)
12 |
13 | Go client for RedisTimeSeries (https://github.com/RedisTimeSeries/redistimeseries), based on redigo.
14 |
15 | Client and ConnPool based on the work of dvirsky and mnunberg on https://github.com/RediSearch/redisearch-go
16 |
17 | ## Installing
18 |
19 | ```sh
20 | $ go get github.com/RedisTimeSeries/redistimeseries-go
21 | ```
22 |
23 | ## Running tests
24 |
25 | A simple test suite is provided, and can be run with:
26 |
27 | ```sh
28 | $ go test
29 | ```
30 |
31 | The tests expect a Redis server with the RedisTimeSeries module loaded to be available at localhost:6379
32 |
33 | ## Example Code
34 |
35 | ```go
36 | package main
37 |
38 | import (
39 | "fmt"
40 | redistimeseries "github.com/RedisTimeSeries/redistimeseries-go"
41 | )
42 |
43 | func main() {
44 | // Connect to localhost with no password
45 | var client = redistimeseries.NewClient("localhost:6379", "nohelp", nil)
46 | var keyname = "mytest"
47 | _, haveit := client.Info(keyname)
48 | if haveit != nil {
49 | client.CreateKeyWithOptions(keyname, redistimeseries.DefaultCreateOptions)
50 | client.CreateKeyWithOptions(keyname+"_avg", redistimeseries.DefaultCreateOptions)
51 | client.CreateRule(keyname, redistimeseries.AvgAggregation, 60, keyname+"_avg")
52 | }
53 | // Add sample with timestamp from server time and value 100
54 | // TS.ADD mytest * 100
55 | _, err := client.AddAutoTs(keyname, 100)
56 | if err != nil {
57 | fmt.Println("Error:", err)
58 | }
59 | }
60 | ```
61 |
62 | ## Supported RedisTimeSeries Commands
63 |
64 | | Command | Recommended API and godoc |
65 | | :--- | ----: |
66 | | [TS.CREATE](https://oss.redislabs.com/redistimeseries/commands/#tscreate) | [CreateKeyWithOptions](https://godoc.org/github.com/RedisTimeSeries/redistimeseries-go#Client.CreateKeyWithOptions) |
67 | | [TS.ALTER](https://oss.redislabs.com/redistimeseries/commands/#tsalter) | [AlterKeyWithOptions](https://godoc.org/github.com/RedisTimeSeries/redistimeseries-go#Client.AlterKeyWithOptions) |
68 | | [TS.ADD](https://oss.redislabs.com/redistimeseries/commands/#tsadd) |
- [Add](https://godoc.org/github.com/RedisTimeSeries/redistimeseries-go#Client.Add)
- [AddAutoTs](https://godoc.org/github.com/RedisTimeSeries/redistimeseries-go#Client.AddAutoTs)
- [AddWithOptions](https://godoc.org/github.com/RedisTimeSeries/redistimeseries-go#Client.AddWithOptions)
- [AddAutoTsWithOptions](https://godoc.org/github.com/RedisTimeSeries/redistimeseries-go#Client.AddWithOptions)
|
69 | | [TS.MADD](https://oss.redislabs.com/redistimeseries/commands/#tsmadd) | [MultiAdd](https://godoc.org/github.com/RedisTimeSeries/redistimeseries-go#Client.MultiAdd) |
70 | | [TS.INCRBY/TS.DECRBY](https://oss.redislabs.com/redistimeseries/commands/#tsincrbytsdecrby) | [IncrBy](https://godoc.org/github.com/RedisTimeSeries/redistimeseries-go#Client.IncrBy) / [DecrBy](https://godoc.org/github.com/RedisTimeSeries/redistimeseries-go#Client.DecrBy) |
71 | | [TS.CREATERULE](https://oss.redislabs.com/redistimeseries/commands/#tscreaterule) | [CreateRule](https://godoc.org/github.com/RedisTimeSeries/redistimeseries-go#Client.CreateRule) |
72 | | [TS.DELETERULE](https://oss.redislabs.com/redistimeseries/commands/#tsdeleterule) | [DeleteRule](https://godoc.org/github.com/RedisTimeSeries/redistimeseries-go#Client.DeleteRule) |
73 | | [TS.RANGE](https://oss.redislabs.com/redistimeseries/commands/#tsrangetsrevrange) | [RangeWithOptions](https://godoc.org/github.com/RedisTimeSeries/redistimeseries-go#Client.RangeWithOptions) |
74 | | [TS.REVRANGE](https://oss.redislabs.com/redistimeseries/commands/#tsrangetsrevrange) | [ReverseRangeWithOptions](https://godoc.org/github.com/RedisTimeSeries/redistimeseries-go#Client.ReverseRangeWithOptions) |
75 | | [TS.MRANGE](https://oss.redislabs.com/redistimeseries/commands/#tsmrangetsmrevrange) | [MultiRangeWithOptions](https://godoc.org/github.com/RedisTimeSeries/redistimeseries-go#Client.MultiRangeWithOptions) |
76 | | [TS.MREVRANGE](https://oss.redislabs.com/redistimeseries/commands/#tsmrangetsmrevrange) | [MultiReverseRangeWithOptions](https://godoc.org/github.com/RedisTimeSeries/redistimeseries-go#Client.MultiReverseRangeWithOptions) |
77 | | [TS.GET](https://oss.redislabs.com/redistimeseries/commands/#tsget) | [Get](https://godoc.org/github.com/RedisTimeSeries/redistimeseries-go#Client.Get) |
78 | | [TS.MGET](https://oss.redislabs.com/redistimeseries/commands/#tsmget) | - [MultiGet](https://godoc.org/github.com/RedisTimeSeries/redistimeseries-go#Client.MultiGet)
- [MultiGetWithOptions](https://godoc.org/github.com/RedisTimeSeries/redistimeseries-go#Client.MultiGetWithOptions)
|
79 | | [TS.INFO](https://oss.redislabs.com/redistimeseries/commands/#tsinfo) | [Info](https://godoc.org/github.com/RedisTimeSeries/redistimeseries-go#Client.Info) |
80 | | [TS.QUERYINDEX](https://oss.redislabs.com/redistimeseries/commands/#tsqueryindex) | [QueryIndex](https://godoc.org/github.com/RedisTimeSeries/redistimeseries-go#Client.QueryIndex) |
81 |
82 |
83 | ## License
84 |
85 | redistimeseries-go is distributed under the Apache-2 license - see [LICENSE](LICENSE)
86 |
--------------------------------------------------------------------------------
/client.go:
--------------------------------------------------------------------------------
1 | package redis_timeseries_go
2 |
3 | import (
4 | "strings"
5 | "time"
6 |
7 | "github.com/gomodule/redigo/redis"
8 | )
9 |
10 | // TODO: refactor this hard limit and revise client locking
11 | // Client Max Connections
12 | var maxConns = 500
13 |
14 | // NewClient creates a new client connecting to the redis host, and using the given name as key prefix.
15 | // Addr can be a single host:port pair, or a comma separated list of host:port,host:port...
16 | // In the case of multiple hosts we create a multi-pool and select connections at random
17 | func NewClient(addr, name string, authPass *string) *Client {
18 | addrs := strings.Split(addr, ",")
19 | var pool ConnPool
20 | if len(addrs) == 1 {
21 | pool = NewSingleHostPool(addrs[0], authPass)
22 | } else {
23 | pool = NewMultiHostPool(addrs, authPass)
24 | }
25 | ret := &Client{
26 | Pool: pool,
27 | Name: name,
28 | }
29 | return ret
30 | }
31 |
32 | // NewClientFromPool creates a new Client with the given pool and client name
33 | func NewClientFromPool(pool *redis.Pool, name string) *Client {
34 | ret := &Client{
35 | Pool: pool,
36 | Name: name,
37 | }
38 | return ret
39 | }
40 |
41 | // CreateKey create a new time-series
42 | // Deprecated: This function has been deprecated, use CreateKeyWithOptions instead
43 | func (client *Client) CreateKey(key string, retentionTime time.Duration) (err error) {
44 | conn := client.Pool.Get()
45 | defer conn.Close()
46 | opts := DefaultCreateOptions
47 | opts.RetentionMSecs = retentionTime
48 | return client.CreateKeyWithOptions(key, opts)
49 | }
50 |
51 | func (client *Client) CreateKeyWithOptions(key string, options CreateOptions) (err error) {
52 | conn := client.Pool.Get()
53 | defer conn.Close()
54 |
55 | args := []interface{}{key}
56 | args, err = options.SerializeSeriesOptions(CREATE_CMD, args)
57 | if err != nil {
58 | return
59 | }
60 | _, err = conn.Do(CREATE_CMD, args...)
61 | return err
62 | }
63 |
64 | // Update the retention, labels of an existing key. The parameters are the same as TS.CREATE.
65 | func (client *Client) AlterKeyWithOptions(key string, options CreateOptions) (err error) {
66 | conn := client.Pool.Get()
67 | defer conn.Close()
68 |
69 | args := []interface{}{key}
70 | args, err = options.SerializeSeriesOptions(ALTER_CMD, args)
71 | if err != nil {
72 | return
73 | }
74 | _, err = conn.Do(ALTER_CMD, args...)
75 | return err
76 | }
77 |
78 | // Add - Append (or create and append) a new sample to the series
79 | // args:
80 | // key - time series key name
81 | // timestamp - time of value
82 | // value - value
83 | func (client *Client) Add(key string, timestamp int64, value float64) (storedTimestamp int64, err error) {
84 | conn := client.Pool.Get()
85 | defer conn.Close()
86 | return redis.Int64(conn.Do(ADD_CMD, key, timestamp, floatToStr(value)))
87 | }
88 |
89 | // AddAutoTs - Append (or create and append) a new sample to the series, with DB automatic timestamp (using the system clock)
90 | // args:
91 | // key - time series key name
92 | // value - value
93 | func (client *Client) AddAutoTs(key string, value float64) (storedTimestamp int64, err error) {
94 | conn := client.Pool.Get()
95 | defer conn.Close()
96 | return redis.Int64(conn.Do(ADD_CMD, key, "*", floatToStr(value)))
97 | }
98 |
99 | // AddWithOptions - Append (or create and append) a new sample to the series, with the specified CreateOptions
100 | // args:
101 | // key - time series key name
102 | // timestamp - time of value
103 | // value - value
104 | // options - define options for create key on add
105 | func (client *Client) AddWithOptions(key string, timestamp int64, value float64, options CreateOptions) (storedTimestamp int64, err error) {
106 | conn := client.Pool.Get()
107 | defer conn.Close()
108 |
109 | args := []interface{}{key, timestamp, floatToStr(value)}
110 | args, err = options.SerializeSeriesOptions(ADD_CMD, args)
111 | if err != nil {
112 | return
113 | }
114 | return redis.Int64(conn.Do(ADD_CMD, args...))
115 | }
116 |
117 | // AddAutoTsWithOptions - Append (or create and append) a new sample to the series, with the specified CreateOptions and DB automatic timestamp (using the system clock)
118 | // args:
119 | // key - time series key name
120 | // value - value
121 | // options - define options for create key on add
122 | func (client *Client) AddAutoTsWithOptions(key string, value float64, options CreateOptions) (storedTimestamp int64, err error) {
123 | conn := client.Pool.Get()
124 | defer conn.Close()
125 | args := []interface{}{key, "*", floatToStr(value)}
126 | args, err = options.SerializeSeriesOptions(ADD_CMD, args)
127 | if err != nil {
128 | return
129 | }
130 | return redis.Int64(conn.Do(ADD_CMD, args...))
131 | }
132 |
133 | // AddWithRetention - append a new value to the series with a duration
134 | // args:
135 | // key - time series key name
136 | // timestamp - time of value
137 | // value - value
138 | // duration - value
139 | // Deprecated: This function has been deprecated, use AddWithOptions instead
140 | func (client *Client) AddWithRetention(key string, timestamp int64, value float64, duration int64) (storedTimestamp int64, err error) {
141 | conn := client.Pool.Get()
142 | defer conn.Close()
143 | options := DefaultCreateOptions
144 | options.RetentionMSecs = time.Duration(duration)
145 | return client.AddWithOptions(key, timestamp, value, options)
146 | }
147 |
148 | // DeleteSerie - deletes series given the time series key name. This API is sugar coating on top of redis DEL command
149 | // args:
150 | // key - time series key name
151 | func (client *Client) DeleteSerie(key string) (err error) {
152 | conn := client.Pool.Get()
153 | defer conn.Close()
154 | _, err = conn.Do(DEL_CMD, key)
155 | return err
156 | }
157 |
158 | // DeleteRange - Delete data points for a given timeseries and interval range in the form of start and end delete timestamps.
159 | // Returns the total deleted datapoints.
160 | // args:
161 | // key - time series key name
162 | // fromTimestamp - start of range. You can use TimeRangeMinimum to express the minimum possible timestamp.
163 | // toTimestamp - end of range. You can use TimeRangeFull or TimeRangeMaximum to express the maximum possible timestamp.
164 | func (client *Client) DeleteRange(key string, fromTimestamp int64, toTimestamp int64) (totalDeletedSamples int64, err error) {
165 | conn := client.Pool.Get()
166 | defer conn.Close()
167 | totalDeletedSamples, err = redis.Int64(conn.Do(TS_DEL_CMD, key, fromTimestamp, toTimestamp))
168 | return
169 | }
170 |
171 | // CreateRule - create a compaction rule
172 | // args:
173 | // sourceKey - key name for source time series
174 | // aggType - AggregationType
175 | // bucketSizeMSec - Time bucket for aggregation in milliseconds
176 | // destinationKey - key name for destination time series
177 | func (client *Client) CreateRule(sourceKey string, aggType AggregationType, bucketSizeMSec uint, destinationKey string) (err error) {
178 | conn := client.Pool.Get()
179 | defer conn.Close()
180 | _, err = conn.Do(CREATERULE_CMD, sourceKey, destinationKey, "AGGREGATION", aggType, bucketSizeMSec)
181 | return err
182 | }
183 |
184 | // DeleteRule - delete a compaction rule
185 | // args:
186 | // sourceKey - key name for source time series
187 | // destinationKey - key name for destination time series
188 | func (client *Client) DeleteRule(sourceKey string, destinationKey string) (err error) {
189 | conn := client.Pool.Get()
190 | defer conn.Close()
191 | _, err = conn.Do(DELETERULE_CMD, sourceKey, destinationKey)
192 | return err
193 | }
194 |
195 | // Range - ranged query
196 | // args:
197 | // key - time series key name
198 | // fromTimestamp - start of range. You can use TimeRangeMinimum to express the minimum possible timestamp.
199 | // toTimestamp - end of range. You can use TimeRangeFull or TimeRangeMaximum to express the maximum possible timestamp.
200 | // Deprecated: This function has been deprecated, use RangeWithOptions instead
201 | func (client *Client) Range(key string, fromTimestamp int64, toTimestamp int64) (dataPoints []DataPoint, err error) {
202 | return client.RangeWithOptions(key, fromTimestamp, toTimestamp, DefaultRangeOptions)
203 |
204 | }
205 |
206 | // AggRange - aggregation over a ranged query
207 | // args:
208 | // key - time series key name
209 | // fromTimestamp - start of range. You can use TimeRangeMinimum to express the minimum possible timestamp.
210 | // toTimestamp - end of range. You can use TimeRangeFull or TimeRangeMaximum to express the maximum possible timestamp.
211 | // aggType - aggregation type
212 | // bucketSizeSec - time bucket for aggregation
213 | // Deprecated: This function has been deprecated, use RangeWithOptions instead
214 | func (client *Client) AggRange(key string, fromTimestamp int64, toTimestamp int64, aggType AggregationType,
215 | bucketSizeSec int) (dataPoints []DataPoint, err error) {
216 | rangeOptions := NewRangeOptions()
217 | rangeOptions = rangeOptions.SetAggregation(aggType, bucketSizeSec)
218 | return client.RangeWithOptions(key, fromTimestamp, toTimestamp, *rangeOptions)
219 | }
220 |
221 | // RangeWithOptions - Query a timestamp range on a specific time-series
222 | // args:
223 | // key - time-series key name
224 | // fromTimestamp - start of range. You can use TimeRangeMinimum to express the minimum possible timestamp.
225 | // toTimestamp - end of range. You can use TimeRangeFull or TimeRangeMaximum to express the maximum possible timestamp.
226 | // rangeOptions - RangeOptions options. You can use the default DefaultRangeOptions
227 | func (client *Client) RangeWithOptions(key string, fromTimestamp int64, toTimestamp int64, rangeOptions RangeOptions) (dataPoints []DataPoint, err error) {
228 | return client.rangeWithOptions(RANGE_CMD, key, fromTimestamp, toTimestamp, rangeOptions)
229 | }
230 |
231 | // ReverseRangeWithOptions - Query a timestamp range on a specific time-series in reverse order
232 | // args:
233 | // key - time-series key name
234 | // fromTimestamp - start of range. You can use TimeRangeMinimum to express the minimum possible timestamp.
235 | // toTimestamp - end of range. You can use TimeRangeFull or TimeRangeMaximum to express the maximum possible timestamp.
236 | // rangeOptions - RangeOptions options. You can use the default DefaultRangeOptions
237 | func (client *Client) ReverseRangeWithOptions(key string, fromTimestamp int64, toTimestamp int64, rangeOptions RangeOptions) (dataPoints []DataPoint, err error) {
238 | return client.rangeWithOptions(REVRANGE_CMD, key, fromTimestamp, toTimestamp, rangeOptions)
239 | }
240 |
241 | // rangeWithOptions - Query a timestamp range on a specific time-series in some order
242 | // args:
243 | // command - range command to run
244 | // key - time-series key name
245 | // fromTimestamp - start of range. You can use TimeRangeMinimum to express the minimum possible timestamp.
246 | // toTimestamp - end of range. You can use TimeRangeFull or TimeRangeMaximum to express the maximum possible timestamp.
247 | // rangeOptions - RangeOptions options. You can use the default DefaultRangeOptions
248 | func (client *Client) rangeWithOptions(command string, key string, fromTimestamp int64, toTimestamp int64, rangeOptions RangeOptions) (dataPoints []DataPoint, err error) {
249 | conn := client.Pool.Get()
250 | defer conn.Close()
251 | var reply interface{}
252 | args := createRangeCmdArguments(key, fromTimestamp, toTimestamp, rangeOptions)
253 | reply, err = conn.Do(command, args...)
254 | if err != nil {
255 | return
256 | }
257 | dataPoints, err = ParseDataPoints(reply)
258 | return
259 | }
260 |
261 | // AggMultiRange - Query a timestamp range across multiple time-series by filters.
262 | // args:
263 | // fromTimestamp - start of range. You can use TimeRangeMinimum to express the minimum possible timestamp.
264 | // toTimestamp - end of range. You can use TimeRangeFull or TimeRangeMaximum to express the maximum possible timestamp.
265 | // aggType - aggregation type
266 | // bucketSizeSec - time bucket for aggregation
267 | // filters - list of filters e.g. "a=bb", "b!=aa"
268 | // Deprecated: This function has been deprecated, use MultiRangeWithOptions instead
269 | func (client *Client) AggMultiRange(fromTimestamp int64, toTimestamp int64, aggType AggregationType,
270 | bucketSizeSec int, filters ...string) (ranges []Range, err error) {
271 | mrangeOptions := NewMultiRangeOptions()
272 | mrangeOptions = mrangeOptions.SetAggregation(aggType, bucketSizeSec)
273 | return client.MultiRangeWithOptions(fromTimestamp, toTimestamp, *mrangeOptions, filters...)
274 | }
275 |
276 | // MultiRangeWithOptions - Query a timestamp range across multiple time-series by filters.
277 | // args:
278 | // fromTimestamp - start of range. You can use TimeRangeMinimum to express the minimum possible timestamp.
279 | // toTimestamp - end of range. You can use TimeRangeFull or TimeRangeMaximum to express the maximum possible timestamp.
280 | // mrangeOptions - MultiRangeOptions options. You can use the default DefaultMultiRangeOptions
281 | // filters - list of filters e.g. "a=bb", "b!=aa"
282 | func (client *Client) MultiRangeWithOptions(fromTimestamp int64, toTimestamp int64, mrangeOptions MultiRangeOptions, filters ...string) (ranges []Range, err error) {
283 | return client.multiRangeWithOptions(MRANGE_CMD, fromTimestamp, toTimestamp, mrangeOptions, filters)
284 | }
285 |
286 | // MultiReverseRangeWithOptions - Query a timestamp range across multiple time-series by filters, in reverse direction.
287 | // args:
288 | // fromTimestamp - start of range. You can use TimeRangeMinimum to express the minimum possible timestamp.
289 | // toTimestamp - end of range. You can use TimeRangeFull or TimeRangeMaximum to express the maximum possible timestamp.
290 | // mrangeOptions - MultiRangeOptions options. You can use the default DefaultMultiRangeOptions
291 | // filters - list of filters e.g. "a=bb", "b!=aa"
292 | func (client *Client) MultiReverseRangeWithOptions(fromTimestamp int64, toTimestamp int64, mrangeOptions MultiRangeOptions, filters ...string) (ranges []Range, err error) {
293 | return client.multiRangeWithOptions(MREVRANGE_CMD, fromTimestamp, toTimestamp, mrangeOptions, filters)
294 | }
295 |
296 | func (client *Client) multiRangeWithOptions(cmd string, fromTimestamp int64, toTimestamp int64, mrangeOptions MultiRangeOptions, filters []string) (ranges []Range, err error) {
297 | conn := client.Pool.Get()
298 | defer conn.Close()
299 | var reply interface{}
300 | args := createMultiRangeCmdArguments(fromTimestamp, toTimestamp, mrangeOptions, filters)
301 | reply, err = conn.Do(cmd, args...)
302 | if err != nil {
303 | return
304 | }
305 | ranges, err = ParseRanges(reply)
306 | return
307 | }
308 |
309 | // Get - Get the last sample of a time-series.
310 | // args:
311 | // key - time-series key name
312 | func (client *Client) Get(key string) (dataPoint *DataPoint,
313 | err error) {
314 | conn := client.Pool.Get()
315 | defer conn.Close()
316 | resp, err := conn.Do(GET_CMD, key)
317 | if err != nil {
318 | return nil, err
319 | }
320 | dataPoint, err = ParseDataPoint(resp)
321 | return
322 | }
323 |
324 | // MultiGet - Get the last sample across multiple time-series, matching the specific filters.
325 | // args:
326 | // filters - list of filters e.g. "a=bb", "b!=aa"
327 | func (client *Client) MultiGet(filters ...string) (ranges []Range, err error) {
328 | return client.MultiGetWithOptions(DefaultMultiGetOptions, filters...)
329 | }
330 |
331 | // MultiGetWithOptions - Get the last samples matching the specific filters.
332 | // args:
333 | // multiGetOptions - MultiGetOptions options. You can use the default DefaultMultiGetOptions
334 | // filters - list of filters e.g. "a=bb", "b!=aa"
335 | func (client *Client) MultiGetWithOptions(multiGetOptions MultiGetOptions, filters ...string) (ranges []Range, err error) {
336 | conn := client.Pool.Get()
337 | defer conn.Close()
338 | var reply interface{}
339 | if len(filters) == 0 {
340 | return
341 | }
342 | args := createMultiGetCmdArguments(multiGetOptions, filters)
343 | reply, err = conn.Do(MGET_CMD, args...)
344 | if err != nil {
345 | return
346 | }
347 | ranges, err = ParseRangesSingleDataPoint(reply)
348 | return
349 | }
350 |
351 | // Returns information and statistics on the time-series.
352 | // args:
353 | // key - time-series key name
354 | func (client *Client) Info(key string) (res KeyInfo, err error) {
355 | conn := client.Pool.Get()
356 | defer conn.Close()
357 | res, err = ParseInfo(conn.Do(INFO_CMD, key))
358 | return res, err
359 | }
360 |
361 | // Get all the keys matching the filter list.
362 | func (client *Client) QueryIndex(filters ...string) (keys []string, err error) {
363 | conn := client.Pool.Get()
364 | defer conn.Close()
365 |
366 | if len(filters) == 0 {
367 | return
368 | }
369 |
370 | args := redis.Args{}
371 | for _, filter := range filters {
372 | args = args.Add(filter)
373 | }
374 | return redis.Strings(conn.Do(QUERYINDEX_CMD, args...))
375 | }
376 |
377 | // Creates a new sample that increments the latest sample's value
378 | func (client *Client) IncrBy(key string, timestamp int64, value float64, options CreateOptions) (int64, error) {
379 | conn := client.Pool.Get()
380 | defer conn.Close()
381 |
382 | args, err := AddCounterArgs(key, timestamp, value, options)
383 | if err != nil {
384 | return -1, err
385 | }
386 | return redis.Int64(conn.Do(INCRBY_CMD, args...))
387 | }
388 |
389 | // Creates a new sample that increments the latest sample's value with an auto timestamp
390 | func (client *Client) IncrByAutoTs(key string, value float64, options CreateOptions) (int64, error) {
391 | conn := client.Pool.Get()
392 | defer conn.Close()
393 |
394 | args, err := AddCounterArgs(key, -1, value, options)
395 | if err != nil {
396 | return -1, err
397 | }
398 | return redis.Int64(conn.Do(INCRBY_CMD, args...))
399 | }
400 |
401 | // Creates a new sample that decrements the latest sample's value
402 | func (client *Client) DecrBy(key string, timestamp int64, value float64, options CreateOptions) (int64, error) {
403 | conn := client.Pool.Get()
404 | defer conn.Close()
405 |
406 | args, err := AddCounterArgs(key, timestamp, value, options)
407 | if err != nil {
408 | return -1, err
409 | }
410 | return redis.Int64(conn.Do(DECRBY_CMD, args...))
411 | }
412 |
413 | // Creates a new sample that decrements the latest sample's value with auto timestamp
414 | func (client *Client) DecrByAutoTs(key string, value float64, options CreateOptions) (int64, error) {
415 | conn := client.Pool.Get()
416 | defer conn.Close()
417 |
418 | args, err := AddCounterArgs(key, -1, value, options)
419 | if err != nil {
420 | return -1, err
421 | }
422 | return redis.Int64(conn.Do(DECRBY_CMD, args...))
423 | }
424 |
425 | // Add counter args for command TS.INCRBY/TS.DECRBY
426 | func AddCounterArgs(key string, timestamp int64, value float64, options CreateOptions) (redis.Args, error) {
427 | args := redis.Args{key, value}
428 | if timestamp > 0 {
429 | args = args.Add("TIMESTAMP", timestamp)
430 | }
431 |
432 | return options.Serialize(args)
433 | }
434 |
435 | // Append new samples to a list of series.
436 | func (client *Client) MultiAdd(samples ...Sample) (timestamps []interface{}, err error) {
437 | conn := client.Pool.Get()
438 | defer conn.Close()
439 |
440 | if len(samples) == 0 {
441 | return
442 | }
443 |
444 | args := redis.Args{}
445 | for _, sample := range samples {
446 | args = args.Add(sample.Key, sample.DataPoint.Timestamp, sample.DataPoint.Value)
447 | }
448 | return redis.Values(conn.Do(MADD_CMD, args...))
449 | }
450 |
--------------------------------------------------------------------------------
/client_test.go:
--------------------------------------------------------------------------------
1 | package redis_timeseries_go
2 |
3 | import (
4 | "log"
5 | "os"
6 | "reflect"
7 | "testing"
8 | "time"
9 |
10 | "github.com/gomodule/redigo/redis"
11 | "github.com/google/go-cmp/cmp"
12 | "github.com/stretchr/testify/assert"
13 | )
14 |
15 | // getRedisTimeSeriesVersion returns RedisTimeSeries version by issuing "MODULE LIST" command
16 | // and iterating through the availabe modules up until "timeseries" is found as the name property
17 | func (c *Client) getRedisTimeSeriesVersion() (version int64, err error) {
18 | conn := client.Pool.Get()
19 | defer conn.Close()
20 | var values []interface{}
21 | var moduleInfo []interface{}
22 | var moduleName string
23 | values, err = redis.Values(conn.Do("MODULE", "LIST"))
24 | if err != nil {
25 | return
26 | }
27 | for _, rawModule := range values {
28 | moduleInfo, err = redis.Values(rawModule, err)
29 | if err != nil {
30 | return
31 | }
32 | moduleName, err = redis.String(moduleInfo[1], err)
33 | if err != nil {
34 | return
35 | }
36 | if moduleName == "timeseries" {
37 | version, err = redis.Int64(moduleInfo[3], err)
38 | }
39 | }
40 | return
41 | }
42 |
43 | func getTestConnectionDetails() (string, string) {
44 | value, exists := os.LookupEnv("REDISTIMESERIES_TEST_HOST")
45 | host := "localhost:6379"
46 | password := ""
47 | valuePassword, existsPassword := os.LookupEnv("REDISTIMESERIES_TEST_PASSWORD")
48 | if exists && value != "" {
49 | host = value
50 | }
51 | if existsPassword && valuePassword != "" {
52 | password = valuePassword
53 | }
54 | return host, password
55 | }
56 |
57 | func createClient() *Client {
58 | host, password := getTestConnectionDetails()
59 | var ptr *string = nil
60 | if len(password) > 0 {
61 | ptr = MakeStringPtr(password)
62 | }
63 | return NewClient(host, "test_client", ptr)
64 | }
65 |
66 | var client = createClient()
67 | var _ = client.FlushAll()
68 |
69 | var defaultDuration, _ = time.ParseDuration("1h")
70 | var tooShortDuration, _ = time.ParseDuration("10ms")
71 |
72 | func (client *Client) FlushAll() (err error) {
73 | conn := client.Pool.Get()
74 | defer conn.Close()
75 | _, err = conn.Do("FLUSHALL")
76 | return err
77 | }
78 |
79 | func TestCreateKey(t *testing.T) {
80 | err := client.FlushAll()
81 | assert.Nil(t, err)
82 | err = client.CreateKey("test_CreateKey", defaultDuration)
83 | assert.Nil(t, err)
84 |
85 | labels := map[string]string{
86 | "cpu": "cpu1",
87 | "country": "IT",
88 | }
89 | err = client.CreateKeyWithOptions("test_CreateKeyLabels", CreateOptions{RetentionMSecs: defaultDuration, Labels: labels})
90 | assert.Nil(t, err)
91 |
92 | err = client.CreateKey("test_CreateKey", tooShortDuration)
93 | assert.NotNil(t, err)
94 |
95 | err = client.CreateKeyWithOptions("test_CreateKeyChunkSize", CreateOptions{ChunkSize: 1024})
96 | assert.Nil(t, err)
97 |
98 | version, err := client.getRedisTimeSeriesVersion()
99 | assert.Nil(t, err)
100 | if version >= 14000 {
101 | var datapoint *DataPoint
102 | info, err := client.Info("test_CreateKeyChunkSize")
103 | assert.Nil(t, err)
104 | assert.Equal(t, int64(1024), info.ChunkSize)
105 |
106 | err = client.CreateKeyWithOptions("test_CreateKey_BlockDuplicatePolicy", CreateOptions{DuplicatePolicy: BlockDuplicatePolicy})
107 | assert.Nil(t, err)
108 | _, err = client.Add("test_CreateKey_BlockDuplicatePolicy", 1, 1.0)
109 | assert.Nil(t, err)
110 | _, err = client.Add("test_CreateKey_BlockDuplicatePolicy", 1, 10.0)
111 | assert.NotNil(t, err)
112 | info, err = client.Info("test_CreateKey_BlockDuplicatePolicy")
113 | assert.Nil(t, err)
114 | assert.Equal(t, BlockDuplicatePolicy, info.DuplicatePolicy)
115 |
116 | err = client.CreateKeyWithOptions("test_CreateKey_FirstDuplicatePolicy", CreateOptions{DuplicatePolicy: FirstDuplicatePolicy})
117 | assert.Nil(t, err)
118 | _, err = client.Add("test_CreateKey_FirstDuplicatePolicy", 1, 1.0)
119 | assert.Nil(t, err)
120 | _, err = client.Add("test_CreateKey_FirstDuplicatePolicy", 1, 10.0)
121 | assert.Nil(t, err)
122 | datapoint, err = client.Get("test_CreateKey_FirstDuplicatePolicy")
123 | assert.Nil(t, err)
124 | assert.Equal(t, 1.0, datapoint.Value)
125 | info, err = client.Info("test_CreateKey_FirstDuplicatePolicy")
126 | assert.Nil(t, err)
127 | assert.Equal(t, FirstDuplicatePolicy, info.DuplicatePolicy)
128 |
129 | err = client.CreateKeyWithOptions("test_CreateKey_LastDuplicatePolicy", CreateOptions{DuplicatePolicy: LastDuplicatePolicy})
130 | assert.Nil(t, err)
131 | _, err = client.Add("test_CreateKey_LastDuplicatePolicy", 1, 1.0)
132 | assert.Nil(t, err)
133 | _, err = client.Add("test_CreateKey_LastDuplicatePolicy", 1, 10.0)
134 | assert.Nil(t, err)
135 | datapoint, err = client.Get("test_CreateKey_LastDuplicatePolicy")
136 | assert.Nil(t, err)
137 | assert.Equal(t, 10.0, datapoint.Value)
138 | info, err = client.Info("test_CreateKey_LastDuplicatePolicy")
139 | assert.Nil(t, err)
140 | assert.Equal(t, LastDuplicatePolicy, info.DuplicatePolicy)
141 |
142 | err = client.CreateKeyWithOptions("test_CreateKey_MinDuplicatePolicy", CreateOptions{DuplicatePolicy: MinDuplicatePolicy})
143 | assert.Nil(t, err)
144 | _, err = client.Add("test_CreateKey_MinDuplicatePolicy", 1, 1.0)
145 | assert.Nil(t, err)
146 | _, err = client.Add("test_CreateKey_MinDuplicatePolicy", 1, 10.0)
147 | assert.Nil(t, err)
148 | datapoint, err = client.Get("test_CreateKey_MinDuplicatePolicy")
149 | assert.Nil(t, err)
150 | assert.Equal(t, 1.0, datapoint.Value)
151 | info, err = client.Info("test_CreateKey_MinDuplicatePolicy")
152 | assert.Nil(t, err)
153 | assert.Equal(t, MinDuplicatePolicy, info.DuplicatePolicy)
154 |
155 | err = client.CreateKeyWithOptions("test_CreateKey_MaxDuplicatePolicy", CreateOptions{DuplicatePolicy: MaxDuplicatePolicy})
156 | assert.Nil(t, err)
157 | _, err = client.Add("test_CreateKey_MaxDuplicatePolicy", 1, 1.0)
158 | assert.Nil(t, err)
159 | _, err = client.Add("test_CreateKey_MaxDuplicatePolicy", 1, 10.0)
160 | assert.Nil(t, err)
161 | datapoint, err = client.Get("test_CreateKey_MaxDuplicatePolicy")
162 | assert.Nil(t, err)
163 | assert.Equal(t, 10.0, datapoint.Value)
164 | info, err = client.Info("test_CreateKey_MaxDuplicatePolicy")
165 | assert.Nil(t, err)
166 | assert.Equal(t, MaxDuplicatePolicy, info.DuplicatePolicy)
167 | }
168 | }
169 |
170 | func TestAlterKey(t *testing.T) {
171 | err := client.FlushAll()
172 | assert.Nil(t, err)
173 | labels := map[string]string{
174 | "cpu": "cpu1",
175 | "country": "IT",
176 | }
177 | err = client.AlterKeyWithOptions("test_AlterKey", CreateOptions{RetentionMSecs: defaultDuration, Labels: labels})
178 | assert.NotNil(t, err)
179 | err = client.CreateKeyWithOptions("test_AlterKey", CreateOptions{RetentionMSecs: defaultDuration, Labels: labels})
180 | assert.Nil(t, err)
181 | err = client.AlterKeyWithOptions("test_AlterKey", CreateOptions{RetentionMSecs: defaultDuration, Labels: labels})
182 | assert.Nil(t, err)
183 | }
184 |
185 | func TestQueryIndex(t *testing.T) {
186 | err := client.FlushAll()
187 | assert.Nil(t, err)
188 | labels := map[string]string{
189 | "sensor_id": "3",
190 | "area_id": "32",
191 | }
192 |
193 | _, err = client.AddWithOptions("test_QueryIndex", 1, 18.7, CreateOptions{Uncompressed: false, Labels: labels})
194 | assert.Nil(t, err)
195 | keys, err := client.QueryIndex("sensor_id=3", "area_id=32")
196 | assert.Nil(t, err)
197 | assert.Equal(t, 1, len(keys))
198 | assert.Equal(t, "test_QueryIndex", keys[0])
199 | keys, err = client.QueryIndex("sensor_id=2")
200 | assert.Nil(t, err)
201 | assert.Equal(t, 0, len(keys))
202 | }
203 |
204 | func TestCreateUncompressedKey(t *testing.T) {
205 | err := client.FlushAll()
206 | assert.Nil(t, err)
207 | compressedKey := "test_Compressed"
208 | uncompressedKey := "test_Uncompressed"
209 | err = client.CreateKeyWithOptions(compressedKey, CreateOptions{Uncompressed: false})
210 | assert.Nil(t, err)
211 | err = client.CreateKeyWithOptions(uncompressedKey, CreateOptions{Uncompressed: true})
212 | assert.Nil(t, err)
213 | var i int64 = 0
214 | for ; i < 1000; i++ {
215 | _, err = client.Add(compressedKey, i, 18.7)
216 | assert.Nil(t, err)
217 | _, err = client.Add(uncompressedKey, i, 18.7)
218 | assert.Nil(t, err)
219 | }
220 | CompressedInfo, _ := client.Info(compressedKey)
221 | UncompressedInfo, _ := client.Info(uncompressedKey)
222 | assert.True(t, CompressedInfo.ChunkCount == 1)
223 | assert.True(t, UncompressedInfo.ChunkCount == 4)
224 |
225 | compressedKey = "test_Compressed_Add"
226 | uncompressedKey = "test_Uncompressed_Add"
227 | for i = 0; i < 1000; i++ {
228 | _, err = client.AddWithOptions(compressedKey, i, 18.7, CreateOptions{Uncompressed: false})
229 | assert.Nil(t, err)
230 | _, err = client.AddWithOptions(uncompressedKey, i, 18.7, CreateOptions{Uncompressed: true})
231 | assert.Nil(t, err)
232 | }
233 | CompressedInfo, _ = client.Info(compressedKey)
234 | UncompressedInfo, _ = client.Info(uncompressedKey)
235 | assert.True(t, CompressedInfo.ChunkCount == 1)
236 | assert.True(t, UncompressedInfo.ChunkCount == 4)
237 | }
238 |
239 | func TestCreateRule(t *testing.T) {
240 | err := client.FlushAll()
241 | assert.Nil(t, err)
242 | var destinationKey string
243 | key := "test_CreateRule"
244 | err = client.CreateKey(key, defaultDuration)
245 | assert.Nil(t, err)
246 | var found bool
247 | for _, aggString := range aggToString {
248 | destinationKey = string("test_CreateRule_dest" + aggString)
249 | err = client.CreateKey(destinationKey, defaultDuration)
250 | assert.Nil(t, err)
251 | err = client.CreateRule(key, aggString, 100, destinationKey)
252 | assert.Nil(t, err)
253 | info, _ := client.Info(key)
254 | found = false
255 | for _, rule := range info.Rules {
256 | if aggString == rule.AggType {
257 | found = true
258 | }
259 | }
260 | assert.True(t, found)
261 | }
262 | }
263 |
264 | func TestClientInfo(t *testing.T) {
265 | err := client.FlushAll()
266 | assert.Nil(t, err)
267 | key := "test_INFO"
268 | destKey := "test_INFO_dest"
269 | err = client.CreateKey(key, defaultDuration)
270 | assert.Nil(t, err)
271 | err = client.CreateKey(destKey, defaultDuration)
272 | assert.Nil(t, err)
273 | err = client.CreateRule(key, AvgAggregation, 100, destKey)
274 | assert.Nil(t, err)
275 | res, err := client.Info(key)
276 | assert.Nil(t, err)
277 | expected := KeyInfo{ChunkCount: 1,
278 | ChunkSize: 4096, LastTimestamp: 0, RetentionTime: 3600000,
279 | Rules: []Rule{{DestKey: destKey, BucketSizeSec: 100, AggType: AvgAggregation}},
280 | Labels: map[string]string{},
281 | }
282 | assert.Equal(t, expected, res)
283 | }
284 |
285 | func TestDeleteRule(t *testing.T) {
286 | err := client.FlushAll()
287 | assert.Nil(t, err)
288 | key := "test_DELETE"
289 | destKey := "test_DELETE_dest"
290 | err = client.CreateKey(key, defaultDuration)
291 | assert.Nil(t, err)
292 | err = client.CreateKey(destKey, defaultDuration)
293 | assert.Nil(t, err)
294 | err = client.CreateRule(key, AvgAggregation, 100, destKey)
295 | assert.Nil(t, err)
296 | err = client.DeleteRule(key, destKey)
297 | assert.Nil(t, err)
298 | info, _ := client.Info(key)
299 | assert.Equal(t, 0, len(info.Rules))
300 | err = client.DeleteRule(key, destKey)
301 | assert.Equal(t, redis.Error("ERR TSDB: compaction rule does not exist"), err)
302 | }
303 |
304 | func TestAdd(t *testing.T) {
305 | err := client.FlushAll()
306 | assert.Nil(t, err)
307 | key := "test_ADD"
308 | now := time.Now().Unix()
309 | PI := 3.14159265359
310 | err = client.CreateKey(key, defaultDuration)
311 | assert.Nil(t, err)
312 | storedTimestamp, err := client.Add(key, now, PI)
313 | assert.Nil(t, err)
314 | assert.Equal(t, now, storedTimestamp)
315 | info, _ := client.Info(key)
316 | assert.Equal(t, now, info.LastTimestamp)
317 |
318 | // Test with auto timestamp
319 | storedTimestamp1, _ := client.AddAutoTs(key, PI)
320 | time.Sleep(1 * time.Millisecond)
321 | storedTimestamp2, _ := client.AddAutoTs(key, PI)
322 | assert.True(t, storedTimestamp1 < storedTimestamp2)
323 |
324 | // Test with auto timestamp with options
325 | storedTimestamp1, _ = client.AddAutoTsWithOptions(key, PI, CreateOptions{RetentionMSecs: defaultDuration})
326 | time.Sleep(1 * time.Millisecond)
327 | storedTimestamp2, _ = client.AddAutoTsWithOptions(key, PI, CreateOptions{RetentionMSecs: defaultDuration})
328 | assert.True(t, storedTimestamp1 < storedTimestamp2)
329 |
330 | version, err := client.getRedisTimeSeriesVersion()
331 | assert.Nil(t, err)
332 | if version >= 14000 {
333 | var datapoint *DataPoint
334 | _, err = client.AddWithOptions("TestAdd_BlockDuplicatePolicy", 1, 1.0, CreateOptions{DuplicatePolicy: BlockDuplicatePolicy})
335 | assert.Nil(t, err)
336 | _, err = client.AddWithOptions("TestAdd_BlockDuplicatePolicy", 1, 10.0, CreateOptions{DuplicatePolicy: BlockDuplicatePolicy})
337 | assert.NotNil(t, err)
338 |
339 | _, err = client.AddWithOptions("TestAdd_FirstDuplicatePolicy", 1, 1.0, CreateOptions{DuplicatePolicy: FirstDuplicatePolicy})
340 | assert.Nil(t, err)
341 | _, err = client.AddWithOptions("TestAdd_FirstDuplicatePolicy", 1, 10.0, CreateOptions{DuplicatePolicy: FirstDuplicatePolicy})
342 | assert.Nil(t, err)
343 | datapoint, err = client.Get("TestAdd_FirstDuplicatePolicy")
344 | assert.Nil(t, err)
345 | assert.Equal(t, 1.0, datapoint.Value)
346 |
347 | _, err = client.AddWithOptions("TestAdd_LastDuplicatePolicy", 1, 1.0, CreateOptions{DuplicatePolicy: LastDuplicatePolicy})
348 | assert.Nil(t, err)
349 | _, err = client.AddWithOptions("TestAdd_LastDuplicatePolicy", 1, 10.0, CreateOptions{DuplicatePolicy: LastDuplicatePolicy})
350 | assert.Nil(t, err)
351 | datapoint, err = client.Get("TestAdd_LastDuplicatePolicy")
352 | assert.Nil(t, err)
353 | assert.Equal(t, 10.0, datapoint.Value)
354 |
355 | _, err = client.AddWithOptions("TestAdd_MinDuplicatePolicy", 1, 1.0, CreateOptions{DuplicatePolicy: MinDuplicatePolicy})
356 | assert.Nil(t, err)
357 | _, err = client.AddWithOptions("TestAdd_MinDuplicatePolicy", 1, 10.0, CreateOptions{DuplicatePolicy: MinDuplicatePolicy})
358 | assert.Nil(t, err)
359 | datapoint, err = client.Get("TestAdd_MinDuplicatePolicy")
360 | assert.Nil(t, err)
361 | assert.Equal(t, 1.0, datapoint.Value)
362 |
363 | _, err = client.AddWithOptions("TestAdd_MaxDuplicatePolicy", 1, 1.0, CreateOptions{DuplicatePolicy: MaxDuplicatePolicy})
364 | assert.Nil(t, err)
365 | _, err = client.AddWithOptions("TestAdd_MaxDuplicatePolicy", 1, 10.0, CreateOptions{DuplicatePolicy: MaxDuplicatePolicy})
366 | assert.Nil(t, err)
367 | datapoint, err = client.Get("TestAdd_MaxDuplicatePolicy")
368 | assert.Nil(t, err)
369 | assert.Equal(t, 10.0, datapoint.Value)
370 | }
371 |
372 | }
373 |
374 | func TestAddWithRetention(t *testing.T) {
375 | err := client.FlushAll()
376 | assert.Nil(t, err)
377 | key := "test_ADDWITHRETENTION"
378 | now := time.Now().Unix()
379 | PI := 3.14159265359
380 | err = client.CreateKey(key, defaultDuration)
381 | assert.Nil(t, err)
382 | _, err = client.AddWithRetention(key, now, PI, 1000000)
383 | assert.Nil(t, err)
384 | info, _ := client.Info(key)
385 | assert.Equal(t, now, info.LastTimestamp)
386 | }
387 |
388 | func TestClient_AggRange(t *testing.T) {
389 | err := client.FlushAll()
390 | assert.Nil(t, err)
391 | key := "test_aggRange"
392 | err = client.CreateKey(key, defaultDuration)
393 | assert.Nil(t, err)
394 | ts1 := int64(1)
395 | ts2 := int64(10)
396 |
397 | value1 := 5.0
398 | value2 := 6.0
399 |
400 | expectedResponse := []DataPoint{{int64(0), 1.0}, {int64(10), 1.0}}
401 |
402 | _, err = client.Add(key, ts1, value1)
403 | assert.Nil(t, err)
404 | _, err = client.Add(key, ts2, value2)
405 | assert.Nil(t, err)
406 |
407 | dataPoints, err := client.AggRange(key, ts1, ts2, CountAggregation, 10)
408 | assert.Nil(t, err)
409 | assert.Equal(t, expectedResponse, dataPoints)
410 |
411 | // ensure zero-based index produces same response
412 | dataPointsZeroBased, err := client.AggRange(key, 0, ts2, CountAggregation, 10)
413 | assert.Nil(t, err)
414 | assert.Equal(t, dataPoints, dataPointsZeroBased)
415 |
416 | }
417 |
418 | func TestClient_AggMultiRange(t *testing.T) {
419 | err := client.FlushAll()
420 | assert.Nil(t, err)
421 | key := "test_aggMultiRange1"
422 | labels := map[string]string{
423 | "cpu": "cpu1",
424 | "country": "US",
425 | }
426 | ts1 := int64(1)
427 | ts2 := int64(2)
428 | _, err = client.AddWithOptions(key, ts1, 5.0, CreateOptions{RetentionMSecs: defaultDuration, Labels: labels})
429 | assert.Nil(t, err)
430 | _, err = client.AddWithOptions(key, ts2, 6.0, CreateOptions{RetentionMSecs: defaultDuration, Labels: labels})
431 | assert.Nil(t, err)
432 | key2 := "test_aggMultiRange2"
433 | labels2 := map[string]string{
434 | "cpu": "cpu2",
435 | "country": "US",
436 | }
437 | err = client.CreateKeyWithOptions(key2, CreateOptions{RetentionMSecs: defaultDuration, Labels: labels2})
438 | assert.Nil(t, err)
439 | _, err = client.AddWithOptions(key2, ts1, 4.0, CreateOptions{})
440 | assert.Nil(t, err)
441 | _, err = client.Add(key2, ts2, 8.0)
442 | assert.Nil(t, err)
443 |
444 | ranges, err := client.AggMultiRange(ts1, ts2, CountAggregation, 10, "country=US")
445 | assert.Nil(t, err)
446 | assert.Equal(t, 2, len(ranges))
447 | assert.Equal(t, 2.0, ranges[0].DataPoints[0].Value)
448 |
449 | _, err = client.AggMultiRange(ts1, ts2, CountAggregation, 10)
450 | assert.NotNil(t, err)
451 |
452 | }
453 |
454 | func TestClient_AggMultiRangeWithOptions(t *testing.T) {
455 | err := client.FlushAll()
456 | assert.Nil(t, err)
457 | key := "test_aggMultiRange1"
458 | labels := map[string]string{
459 | "cpu": "cpu1",
460 | "country": "US",
461 | }
462 | ts1 := int64(1)
463 | ts2 := int64(2)
464 | _, err = client.AddWithOptions(key, ts1, 1, CreateOptions{RetentionMSecs: defaultDuration, Labels: labels})
465 | assert.Nil(t, err)
466 | _, err = client.AddWithOptions(key, ts2, 2, CreateOptions{RetentionMSecs: defaultDuration, Labels: labels})
467 | assert.Nil(t, err)
468 |
469 | key2 := "test_aggMultiRange2"
470 | labels2 := map[string]string{
471 | "cpu": "cpu2",
472 | "country": "US",
473 | }
474 | err = client.CreateKeyWithOptions(key2, CreateOptions{RetentionMSecs: defaultDuration, Labels: labels2})
475 | assert.Nil(t, err)
476 | _, err = client.AddWithOptions(key2, ts1, 1, CreateOptions{})
477 | assert.Nil(t, err)
478 | _, err = client.Add(key2, ts2, 2)
479 | assert.Nil(t, err)
480 |
481 | ranges, err := client.MultiRangeWithOptions(ts1, ts2, DefaultMultiRangeOptions, "country=US")
482 | assert.Nil(t, err)
483 | assert.Equal(t, 2, len(ranges))
484 | }
485 |
486 | func TestClient_Get(t *testing.T) {
487 | err := client.FlushAll()
488 | assert.Nil(t, err)
489 | keyWithData := "test_TestClient_Get_keyWithData"
490 | keyEmpty := "test_TestClient_Get_Empty_Key"
491 | noKey := "test_TestClient_Get_dontexist"
492 |
493 | err = client.CreateKeyWithOptions(keyEmpty, DefaultCreateOptions)
494 | if err != nil {
495 | t.Errorf("TestClient_Get CreateKeyWithOptions() error = %v", err)
496 | return
497 | }
498 |
499 | _, err = client.AddWithOptions(keyWithData, 1, 5.0, DefaultCreateOptions)
500 | if err != nil {
501 | t.Errorf("TestClient_Get AddWithOptions() error = %v", err)
502 | return
503 | }
504 |
505 | type fields struct {
506 | Pool ConnPool
507 | Name string
508 | }
509 | type args struct {
510 | key string
511 | }
512 | tests := []struct {
513 | name string
514 | fields fields
515 | args args
516 | wantDataPoint *DataPoint
517 | wantErr bool
518 | }{
519 | {"empty key", fields{client.Pool, "test"}, args{keyEmpty}, nil, false},
520 | {"key with value", fields{client.Pool, "test"}, args{keyWithData}, &DataPoint{1, 5.0}, false},
521 | {"no key error", fields{client.Pool, "test"}, args{noKey}, nil, true},
522 | }
523 | for _, tt := range tests {
524 | t.Run(tt.name, func(t *testing.T) {
525 | client := &Client{
526 | Pool: tt.fields.Pool,
527 | Name: tt.fields.Name,
528 | }
529 | gotDataPoint, err := client.Get(tt.args.key)
530 | if (err != nil) != tt.wantErr {
531 | t.Errorf("Get() error = %v, wantErr %v", err, tt.wantErr)
532 | return
533 | }
534 | if tt.wantErr == false {
535 | if !reflect.DeepEqual(gotDataPoint, tt.wantDataPoint) && tt.wantErr == false {
536 | t.Errorf("Get() gotDataPoint = %v, want %v", gotDataPoint, tt.wantDataPoint)
537 | }
538 | }
539 | })
540 | }
541 | }
542 |
543 | func TestClient_MultiGet(t *testing.T) {
544 | err := client.FlushAll()
545 | assert.Nil(t, err)
546 | key1 := "test_TestClient_MultiGet_key1"
547 | key2 := "test_TestClient_MultiGet_key2"
548 | labels1 := map[string]string{
549 | "metric": "cpu",
550 | "country": "US",
551 | }
552 | labels2 := map[string]string{
553 | "metric": "cpu",
554 | "country": "UK",
555 | }
556 |
557 | _, err = client.AddWithOptions(key1, 1, 5.0, CreateOptions{Labels: labels1})
558 | if err != nil {
559 | t.Errorf("TestClient_MultiGet Add() error = %v", err)
560 | return
561 | }
562 | _, err = client.Add(key1, 2, 15.0)
563 | assert.Nil(t, err)
564 | _, err = client.Add(key1, 3, 15.0)
565 | assert.Nil(t, err)
566 | _, err = client.AddWithOptions(key2, 1, 5.0, CreateOptions{Labels: labels2})
567 | assert.Nil(t, err)
568 |
569 | type fields struct {
570 | Pool ConnPool
571 | Name string
572 | }
573 | type args struct {
574 | filters []string
575 | }
576 | tests := []struct {
577 | name string
578 | fields fields
579 | args args
580 | wantRanges []Range
581 | wantErr bool
582 | }{
583 | {"multi key", fields{client.Pool, "test"}, args{[]string{"metric=cpu", "country=UK"}}, []Range{Range{key2, map[string]string{}, []DataPoint{DataPoint{1, 5.0}}}}, false},
584 | }
585 | for _, tt := range tests {
586 | t.Run(tt.name, func(t *testing.T) {
587 | client := &Client{
588 | Pool: tt.fields.Pool,
589 | Name: tt.fields.Name,
590 | }
591 | gotRanges, err := client.MultiGet(tt.args.filters...)
592 | if (err != nil) != tt.wantErr {
593 | t.Errorf("MultiGet() error = %v, wantErr %v", err, tt.wantErr)
594 | return
595 | }
596 | if !reflect.DeepEqual(gotRanges, tt.wantRanges) {
597 | t.Errorf("MultiGet() gotRanges = %v, want %v", gotRanges, tt.wantRanges)
598 | }
599 | })
600 | }
601 | }
602 |
603 | func TestClient_MultiGetWithOptions(t *testing.T) {
604 | err := client.FlushAll()
605 | assert.Nil(t, err)
606 | key1 := "test_TestClient_MultiGet_key1"
607 | key2 := "test_TestClient_MultiGet_key2"
608 | labels1 := map[string]string{
609 | "metric": "cpu",
610 | "country": "US",
611 | }
612 | labels2 := map[string]string{
613 | "metric": "cpu",
614 | "country": "UK",
615 | }
616 |
617 | _, err = client.AddWithOptions(key1, 1, 5.0, CreateOptions{Labels: labels1})
618 | assert.Nil(t, err)
619 | _, err = client.Add(key1, 2, 15.0)
620 | assert.Nil(t, err)
621 | _, err = client.Add(key1, 3, 15.0)
622 | assert.Nil(t, err)
623 | _, err = client.AddWithOptions(key2, 1, 5.0, CreateOptions{Labels: labels2})
624 | assert.Nil(t, err)
625 |
626 | type fields struct {
627 | Pool ConnPool
628 | Name string
629 | }
630 | type args struct {
631 | filters []string
632 | }
633 | tests := []struct {
634 | name string
635 | fields fields
636 | args args
637 | wantRanges []Range
638 | wantErr bool
639 | }{
640 | {"multi key", fields{client.Pool, "test"}, args{[]string{"metric=cpu", "country=UK"}}, []Range{Range{key2, map[string]string{"country": "UK", "metric": "cpu"}, []DataPoint{DataPoint{1, 5.0}}}}, false},
641 | }
642 | for _, tt := range tests {
643 | t.Run(tt.name, func(t *testing.T) {
644 | client := &Client{
645 | Pool: tt.fields.Pool,
646 | Name: tt.fields.Name,
647 | }
648 | gotRanges, err := client.MultiGetWithOptions(*NewMultiGetOptions().SetWithLabels(true), tt.args.filters...)
649 | if (err != nil) != tt.wantErr {
650 | t.Errorf("MultiGet() error = %v, wantErr %v", err, tt.wantErr)
651 | return
652 | }
653 | if !reflect.DeepEqual(gotRanges, tt.wantRanges) {
654 | t.Errorf("MultiGet() gotRanges = %v, want %v", gotRanges, tt.wantRanges)
655 | }
656 | })
657 | }
658 | }
659 |
660 | func TestClient_Range(t *testing.T) {
661 | err := client.FlushAll()
662 | assert.Nil(t, err)
663 | key1 := "TestClient_Range_key1"
664 | key2 := "TestClient_Range_key2"
665 | err = client.CreateKeyWithOptions(key1, DefaultCreateOptions)
666 | assert.Nil(t, err)
667 | err = client.CreateKeyWithOptions(key2, DefaultCreateOptions)
668 | assert.Nil(t, err)
669 |
670 | _, err = client.Add(key1, 1, 5)
671 | assert.Nil(t, err)
672 | _, err = client.Add(key1, 2, 10)
673 | assert.Nil(t, err)
674 | type fields struct {
675 | Pool ConnPool
676 | Name string
677 | }
678 | type args struct {
679 | key string
680 | fromTimestamp int64
681 | toTimestamp int64
682 | }
683 | tests := []struct {
684 | name string
685 | fields fields
686 | args args
687 | wantDataPoints []DataPoint
688 | wantErr bool
689 | }{
690 | {"multi points", fields{client.Pool, "test"}, args{key1, 1, 2}, []DataPoint{{1, 5}, {2, 10}}, false},
691 | {"empty serie", fields{client.Pool, "test"}, args{key2, 1, 2}, []DataPoint{}, false},
692 | }
693 | for _, tt := range tests {
694 | t.Run(tt.name, func(t *testing.T) {
695 | client := &Client{
696 | Pool: tt.fields.Pool,
697 | Name: tt.fields.Name,
698 | }
699 | gotDataPoints, err := client.Range(tt.args.key, tt.args.fromTimestamp, tt.args.toTimestamp)
700 | if (err != nil) != tt.wantErr {
701 | t.Errorf("Range() error = %v, wantErr %v", err, tt.wantErr)
702 | return
703 | }
704 | assert.Equal(t, gotDataPoints, tt.wantDataPoints)
705 | if !reflect.DeepEqual((gotDataPoints), tt.wantDataPoints) {
706 | t.Errorf("Range() gotDataPoints = %v, want %v", (gotDataPoints), tt.wantDataPoints)
707 | }
708 | })
709 | }
710 | }
711 |
712 | func TestNewClientFromPool(t *testing.T) {
713 | host, password := getTestConnectionDetails()
714 | pool := &redis.Pool{Dial: func() (redis.Conn, error) {
715 | return redis.Dial("tcp", host, redis.DialPassword(password))
716 | }, MaxIdle: maxConns}
717 | client1 := NewClientFromPool(pool, "cs-client-1")
718 | client2 := NewClientFromPool(pool, "ts-client-2")
719 | assert.Equal(t, client1.Pool, client2.Pool)
720 | err1 := client1.Pool.Close()
721 | err2 := client2.Pool.Close()
722 | assert.Nil(t, err1)
723 | assert.Nil(t, err2)
724 | }
725 |
726 | func TestIncrDecrByAutoTs(t *testing.T) {
727 | tkey := "Test:IncrDecrByAutoTs"
728 | err := client.FlushAll()
729 | assert.Nil(t, err)
730 | storedTimestamp1, _ := client.IncrByAutoTs(tkey, 101, CreateOptions{Uncompressed: false, Labels: map[string]string{}})
731 | time.Sleep(1 * time.Millisecond)
732 | log.Printf("YO: %+v\n", storedTimestamp1)
733 | storedTimestamp2, _ := client.DecrByAutoTs(tkey, 1, CreateOptions{Uncompressed: false, Labels: map[string]string{}})
734 | assert.True(t, storedTimestamp1 < storedTimestamp2)
735 | datapoint, _ := client.Get(tkey)
736 | assert.True(t, datapoint.Value == 100)
737 | }
738 |
739 | func TestIncrDecrBy(t *testing.T) {
740 | err := client.FlushAll()
741 | assert.Nil(t, err)
742 | labels := map[string]string{
743 | "sensor_id": "3",
744 | "area_id": "32",
745 | }
746 |
747 | currentTimestamp := time.Now().UnixNano() / 1e6
748 | timestamp, err := client.IncrBy("Test:IncrDecrBy", currentTimestamp, 13, CreateOptions{Uncompressed: false, Labels: labels})
749 | assert.Nil(t, err)
750 | assert.Equal(t, currentTimestamp, timestamp)
751 |
752 | timestamp, err = client.DecrBy("Test:IncrDecrBy", currentTimestamp+1, 14, CreateOptions{Uncompressed: false, Labels: labels})
753 | assert.Nil(t, err)
754 | assert.Equal(t, currentTimestamp+1, timestamp)
755 | }
756 |
757 | func TestMultiAdd(t *testing.T) {
758 | err := client.FlushAll()
759 | assert.Nil(t, err)
760 |
761 | currentTimestamp := time.Now().UnixNano() / 1e6
762 | _, err = client.AddWithOptions("test:MultiAdd", currentTimestamp, 18.7, CreateOptions{Uncompressed: false})
763 | assert.Nil(t, err)
764 | values, err := client.MultiAdd(Sample{Key: "test:MultiAdd", DataPoint: DataPoint{Timestamp: currentTimestamp + 1, Value: 14}},
765 | Sample{Key: "test:MultiAdd", DataPoint: DataPoint{Timestamp: currentTimestamp + 2, Value: 15}})
766 | assert.Nil(t, err)
767 | assert.Equal(t, 2, len(values))
768 | assert.Equal(t, currentTimestamp+1, values[0])
769 | assert.Equal(t, currentTimestamp+2, values[1])
770 |
771 | values, err = client.MultiAdd(Sample{Key: "test:MultiAdd", DataPoint: DataPoint{Timestamp: currentTimestamp + 3, Value: 14}},
772 | Sample{Key: "test:MultiAdd:notExit", DataPoint: DataPoint{Timestamp: currentTimestamp + 4, Value: 14}})
773 | assert.Nil(t, err)
774 | assert.Equal(t, 2, len(values))
775 | assert.Equal(t, currentTimestamp+3, values[0])
776 | v, ok := values[1].(error)
777 | assert.NotNil(t, v)
778 | assert.True(t, ok)
779 |
780 | values, err = client.MultiAdd()
781 | assert.Nil(t, values)
782 | assert.Nil(t, err)
783 | }
784 |
785 | func TestClient_ReverseRangeWithOptions(t *testing.T) {
786 | err := client.FlushAll()
787 | assert.Nil(t, err)
788 | key1 := "TestClient_RevRange_key1"
789 | key2 := "TestClient_RevRange_key2"
790 | err = client.CreateKeyWithOptions(key1, DefaultCreateOptions)
791 | assert.Nil(t, err)
792 | err = client.CreateKeyWithOptions(key2, DefaultCreateOptions)
793 | assert.Nil(t, err)
794 |
795 | _, err = client.Add(key1, 1, 5)
796 | assert.Nil(t, err)
797 |
798 | _, err = client.Add(key1, 2, 10)
799 | assert.Nil(t, err)
800 |
801 | type fields struct {
802 | Pool ConnPool
803 | Name string
804 | }
805 | type args struct {
806 | key string
807 | fromTimestamp int64
808 | toTimestamp int64
809 | rangeOptions RangeOptions
810 | }
811 | tests := []struct {
812 | name string
813 | fields fields
814 | args args
815 | wantDataPoints []DataPoint
816 | wantErr bool
817 | }{
818 | {"multi points", fields{client.Pool, "test"}, args{key1, 1, 2, DefaultRangeOptions}, []DataPoint{{2, 10}, {1, 5}}, false},
819 | {"last point only", fields{client.Pool, "test"}, args{key1, 1, 2, *NewRangeOptions().SetCount(1)}, []DataPoint{{2, 10}}, false},
820 | {"empty serie", fields{client.Pool, "test"}, args{key2, 1, 2, DefaultRangeOptions}, []DataPoint{}, false},
821 | {"bad range", fields{client.Pool, "test"}, args{key2, 1, 0, DefaultRangeOptions}, []DataPoint{}, false},
822 | }
823 | for _, tt := range tests {
824 | t.Run(tt.name, func(t *testing.T) {
825 | client := &Client{
826 | Pool: tt.fields.Pool,
827 | Name: tt.fields.Name,
828 | }
829 | gotDataPoints, err := client.ReverseRangeWithOptions(tt.args.key, tt.args.fromTimestamp, tt.args.toTimestamp, tt.args.rangeOptions)
830 | if (err != nil) != tt.wantErr {
831 | t.Errorf("ReverseRangeWithOptions() error = %v, wantErr %v", err, tt.wantErr)
832 | return
833 | }
834 | if !reflect.DeepEqual(gotDataPoints, tt.wantDataPoints) {
835 | t.Errorf("ReverseRangeWithOptions() gotDataPoints = %v, want %v", gotDataPoints, tt.wantDataPoints)
836 | }
837 | })
838 | }
839 | }
840 |
841 | func TestClient_RangeWithOptions(t *testing.T) {
842 | err := client.FlushAll()
843 | assert.Nil(t, err)
844 | key1 := "TestClient_RangeWithOptions_key1"
845 | key2 := "TestClient_RangeWithOptions_key2"
846 | err = client.CreateKeyWithOptions(key1, DefaultCreateOptions)
847 | assert.Nil(t, err)
848 | err = client.CreateKeyWithOptions(key2, DefaultCreateOptions)
849 | assert.Nil(t, err)
850 |
851 | _, err = client.Add(key1, 1, 5)
852 | assert.Nil(t, err)
853 | _, err = client.Add(key1, 2, 10)
854 | assert.Nil(t, err)
855 |
856 | type fields struct {
857 | Pool ConnPool
858 | Name string
859 | }
860 | type args struct {
861 | key string
862 | fromTimestamp int64
863 | toTimestamp int64
864 | rangeOptions RangeOptions
865 | }
866 | tests := []struct {
867 | name string
868 | fields fields
869 | args args
870 | wantDataPoints []DataPoint
871 | wantErr bool
872 | }{
873 | {"multi points", fields{client.Pool, "test"}, args{key1, 1, 2, DefaultRangeOptions}, []DataPoint{{1, 5}, {2, 10}}, false},
874 | {"first point only", fields{client.Pool, "test"}, args{key1, 1, 2, *NewRangeOptions().SetCount(1)}, []DataPoint{{1, 5}}, false},
875 | {"empty serie", fields{client.Pool, "test"}, args{key2, 1, 2, DefaultRangeOptions}, []DataPoint{}, false},
876 | {"bad range", fields{client.Pool, "test"}, args{key2, 1, 0, DefaultRangeOptions}, []DataPoint{}, false},
877 | }
878 | for _, tt := range tests {
879 | t.Run(tt.name, func(t *testing.T) {
880 | client := &Client{
881 | Pool: tt.fields.Pool,
882 | Name: tt.fields.Name,
883 | }
884 | gotDataPoints, err := client.RangeWithOptions(tt.args.key, tt.args.fromTimestamp, tt.args.toTimestamp, tt.args.rangeOptions)
885 | if (err != nil) != tt.wantErr {
886 | t.Errorf("RangeWithOptions() error = %v, wantErr %v", err, tt.wantErr)
887 | return
888 | }
889 | if !reflect.DeepEqual(gotDataPoints, tt.wantDataPoints) {
890 | t.Errorf("RangeWithOptions() gotDataPoints = %v, want %v", gotDataPoints, tt.wantDataPoints)
891 | }
892 | })
893 | }
894 | }
895 |
896 | func TestClient_MultiReverseRangeWithOptions(t *testing.T) {
897 | err := client.FlushAll()
898 | assert.Nil(t, err)
899 | key1 := "test_TestClient_MultiReverseRangeWithOptions_key1"
900 | key2 := "test_TestClient_MultiReverseRangeWithOptions_key2"
901 | labels1 := map[string]string{
902 | "metric": "cpu",
903 | "country": "US",
904 | }
905 | labels2 := map[string]string{
906 | "metric": "cpu",
907 | "country": "UK",
908 | }
909 |
910 | _, err = client.AddWithOptions(key1, 1, 5.0, CreateOptions{Labels: labels1})
911 | assert.Nil(t, err)
912 | _, err = client.Add(key1, 2, 15.0)
913 | assert.Nil(t, err)
914 | _, err = client.AddWithOptions(key2, 1, 5.0, CreateOptions{Labels: labels2})
915 | assert.Nil(t, err)
916 |
917 | type fields struct {
918 | Pool ConnPool
919 | Name string
920 | }
921 | type args struct {
922 | fromTimestamp int64
923 | toTimestamp int64
924 | mrangeOptions MultiRangeOptions
925 | filters []string
926 | }
927 | tests := []struct {
928 | name string
929 | fields fields
930 | args args
931 | wantRanges []Range
932 | wantErr bool
933 | }{
934 | {"error one matcher", fields{client.Pool, "test"}, args{1, 2, DefaultMultiRangeOptions, []string{}}, nil, true},
935 | {"last point only single serie", fields{client.Pool, "test"}, args{1, 2, *NewMultiRangeOptions().SetCount(1), []string{"country=UK"}}, []Range{{key2, map[string]string{}, []DataPoint{DataPoint{1, 5.0}}}}, false},
936 | {"multi series", fields{client.Pool, "test"}, args{1, 2, DefaultMultiRangeOptions, []string{"metric=cpu"}}, []Range{Range{key1, map[string]string{}, []DataPoint{{2, 15.0}, {1, 5.0}}}, {key2, map[string]string{}, []DataPoint{DataPoint{1, 5.0}}}}, false},
937 | {"last point only multi series", fields{client.Pool, "test"}, args{1, 2, *NewMultiRangeOptions().SetCount(1), []string{"metric=cpu"}}, []Range{Range{key1, map[string]string{}, []DataPoint{{2, 15.0}}}, {key2, map[string]string{}, []DataPoint{DataPoint{1, 5.0}}}}, false},
938 | }
939 | for _, tt := range tests {
940 | t.Run(tt.name, func(t *testing.T) {
941 | client := &Client{
942 | Pool: tt.fields.Pool,
943 | Name: tt.fields.Name,
944 | }
945 | gotRanges, err := client.MultiReverseRangeWithOptions(tt.args.fromTimestamp, tt.args.toTimestamp, tt.args.mrangeOptions, tt.args.filters...)
946 | if (err != nil) != tt.wantErr {
947 | t.Errorf("MultiReverseRangeWithOptions() error = %v, wantErr %v", err, tt.wantErr)
948 | return
949 | }
950 | if !cmp.Equal(gotRanges, tt.wantRanges) {
951 | t.Errorf("MultiReverseRangeWithOptions() gotRanges = %v, want %v. Difference: %s", gotRanges, tt.wantRanges, cmp.Diff(gotRanges, tt.wantRanges))
952 | }
953 | })
954 | }
955 | }
956 |
957 | func TestClient_DeleteRange(t *testing.T) {
958 |
959 | err := client.FlushAll()
960 | assert.Nil(t, err)
961 | key1 := "test_TestClient_DeleteRange_key1"
962 | key2 := "test_TestClient_DeleteRange_key2"
963 | key3 := "test_TestClient_DeleteRange_key3"
964 | labels1 := map[string]string{
965 | "metric": "cpu",
966 | "country": "US",
967 | }
968 | labels2 := map[string]string{
969 | "metric": "cpu",
970 | "country": "UK",
971 | }
972 |
973 | _, err = client.AddWithOptions(key1, 1, 5.0, CreateOptions{Labels: labels1})
974 | assert.Nil(t, err)
975 | _, err = client.Add(key1, 2, 15.0)
976 | assert.Nil(t, err)
977 | _, err = client.Add(key1, 10, 15.0)
978 | assert.Nil(t, err)
979 | _, err = client.AddWithOptions(key2, 1, 5.0, CreateOptions{Labels: labels2})
980 | assert.Nil(t, err)
981 | _, err = client.Add(key2, 10, 15.0)
982 | assert.Nil(t, err)
983 | _, err = client.Add(key3, 10, 15.0)
984 | assert.Nil(t, err)
985 | _, err = client.Add(key3, 11, 15.0)
986 | assert.Nil(t, err)
987 |
988 | type fields struct {
989 | Pool ConnPool
990 | Name string
991 | }
992 | type args struct {
993 | key string
994 | fromTimestamp int64
995 | toTimestamp int64
996 | }
997 | tests := []struct {
998 | name string
999 | fields fields
1000 | args args
1001 | wantErr bool
1002 | wantDeletedSamples int
1003 | wantFinalCount int
1004 | }{
1005 | {"delete2Datapoints", fields{client.Pool, "test"}, args{key1, 1, 2}, false, 2, 1},
1006 | {"deleteAllDatapoints", fields{client.Pool, "test"}, args{key2, 1, 100}, false, 2, 0},
1007 | {"deleteNoDatapoints", fields{client.Pool, "test"}, args{key3, 1, 5}, false, 0, 2},
1008 | }
1009 | for _, tt := range tests {
1010 | t.Run(tt.name, func(t *testing.T) {
1011 | client := &Client{
1012 | Pool: tt.fields.Pool,
1013 | Name: tt.fields.Name,
1014 | }
1015 | totalDeletedSamples, err := client.DeleteRange(tt.args.key, tt.args.fromTimestamp, tt.args.toTimestamp)
1016 | if (err != nil) != tt.wantErr {
1017 | t.Errorf("DeleteRange() error = %v, wantErr %v", err, tt.wantErr)
1018 | }
1019 | if totalDeletedSamples != int64(tt.wantDeletedSamples) {
1020 | t.Errorf("DeleteRange() wanted total deleted samples of %d and got %d", tt.wantDeletedSamples, totalDeletedSamples)
1021 | }
1022 | datapoints, err := client.Range(tt.args.key, TimeRangeMinimum, TimeRangeMaximum)
1023 | if err != nil {
1024 | t.Errorf("DeleteRange() Range error = %v", err)
1025 | }
1026 | if len(datapoints) != tt.wantFinalCount {
1027 | t.Errorf("DeleteRange() wanted a final series datapoints count of %d and got %d", tt.wantFinalCount, len(datapoints))
1028 | }
1029 | })
1030 | }
1031 | }
1032 |
--------------------------------------------------------------------------------
/common.go:
--------------------------------------------------------------------------------
1 | package redis_timeseries_go
2 |
3 | import (
4 | "fmt"
5 | "math"
6 | "strconv"
7 | "time"
8 | )
9 |
10 | //go:generate stringer -type=AggregationType
11 | type AggregationType string
12 |
13 | //go:generate stringer -type=ReducerType
14 | type ReducerType string
15 |
16 | //go:generate stringer -type=DuplicatePolicyType
17 | type DuplicatePolicyType string
18 |
19 | const (
20 | SumReducer ReducerType = "SUM"
21 | MinReducer ReducerType = "MIN"
22 | MaxReducer ReducerType = "MAX"
23 | )
24 |
25 | const (
26 | AvgAggregation AggregationType = "AVG"
27 | SumAggregation AggregationType = "SUM"
28 | MinAggregation AggregationType = "MIN"
29 | MaxAggregation AggregationType = "MAX"
30 | CountAggregation AggregationType = "COUNT"
31 | FirstAggregation AggregationType = "FIRST"
32 | LastAggregation AggregationType = "LAST"
33 | StdPAggregation AggregationType = "STD.P"
34 | StdSAggregation AggregationType = "STD.S"
35 | VarPAggregation AggregationType = "VAR.P"
36 | VarSAggregation AggregationType = "VAR.S"
37 | )
38 |
39 | const (
40 | CREATE_CMD string = "TS.CREATE"
41 | ALTER_CMD string = "TS.ALTER"
42 | ADD_CMD string = "TS.ADD"
43 | MADD_CMD string = "TS.MADD"
44 | INCRBY_CMD string = "TS.INCRBY"
45 | DECRBY_CMD string = "TS.DECRBY"
46 | CREATERULE_CMD string = "TS.CREATERULE"
47 | DELETERULE_CMD string = "TS.DELETERULE"
48 | RANGE_CMD string = "TS.RANGE"
49 | REVRANGE_CMD string = "TS.REVRANGE"
50 | MRANGE_CMD string = "TS.MRANGE"
51 | MREVRANGE_CMD string = "TS.MREVRANGE"
52 | GET_CMD string = "TS.GET"
53 | MGET_CMD string = "TS.MGET"
54 | INFO_CMD string = "TS.INFO"
55 | QUERYINDEX_CMD string = "TS.QUERYINDEX"
56 | DEL_CMD string = "DEL"
57 | TS_DEL_CMD string = "TS.DEL"
58 | )
59 |
60 | // Check https://oss.redislabs.com/redistimeseries/configuration/#duplicate_policy for more inforamtion about duplicate policies
61 | const (
62 | BlockDuplicatePolicy DuplicatePolicyType = "block" // an error will occur for any out of order sample
63 | FirstDuplicatePolicy DuplicatePolicyType = "first" // ignore the new value
64 | LastDuplicatePolicy DuplicatePolicyType = "last" // override with latest value
65 | MinDuplicatePolicy DuplicatePolicyType = "min" // only override if the value is lower than the existing value
66 | MaxDuplicatePolicy DuplicatePolicyType = "max" // only override if the value is higher than the existing value
67 | )
68 |
69 | var aggToString = []AggregationType{AvgAggregation, SumAggregation, MinAggregation, MaxAggregation, CountAggregation, FirstAggregation, LastAggregation, StdPAggregation, StdSAggregation, VarPAggregation, VarSAggregation}
70 |
71 | // CreateOptions are a direct mapping to the options provided when creating a new time-series
72 | // Check https://oss.redislabs.com/redistimeseries/1.4/commands/#tscreate for a detailed description
73 | type CreateOptions struct {
74 | Uncompressed bool
75 | RetentionMSecs time.Duration
76 | Labels map[string]string
77 | ChunkSize int64
78 | DuplicatePolicy DuplicatePolicyType
79 | }
80 |
81 | var DefaultCreateOptions = CreateOptions{
82 | Uncompressed: false,
83 | RetentionMSecs: 0,
84 | Labels: map[string]string{},
85 | ChunkSize: 0,
86 | DuplicatePolicy: "",
87 | }
88 |
89 | // Client is an interface to time series redis commands
90 | type Client struct {
91 | Pool ConnPool
92 | Name string
93 | }
94 |
95 | const TimeRangeMinimum = 0
96 | const TimeRangeMaximum = math.MaxInt64
97 | const TimeRangeFull = int64(-1)
98 |
99 | type Rule struct {
100 | DestKey string
101 | BucketSizeSec int
102 | AggType AggregationType
103 | }
104 |
105 | type KeyInfo struct {
106 | ChunkCount int64
107 | MaxSamplesPerChunk int64 // As of RedisTimeseries >= v1.4 MaxSamplesPerChunk is deprecated in favor of ChunkSize
108 | ChunkSize int64
109 | LastTimestamp int64
110 | RetentionTime int64
111 | Rules []Rule
112 | Labels map[string]string
113 | DuplicatePolicy DuplicatePolicyType // Duplicate sample policy
114 | }
115 |
116 | type DataPoint struct {
117 | Timestamp int64
118 | Value float64
119 | }
120 |
121 | type Sample struct {
122 | Key string
123 | DataPoint DataPoint
124 | }
125 |
126 | func NewDataPoint(timestamp int64, value float64) *DataPoint {
127 | return &DataPoint{Timestamp: timestamp, Value: value}
128 | }
129 |
130 | type Range struct {
131 | Name string
132 | Labels map[string]string
133 | DataPoints []DataPoint
134 | }
135 |
136 | // Serialize options to args. Given that DUPLICATE_POLICY and ON_DUPLICATE depend upon the issuing command we need to specify the command for which we are generating the args for
137 | func (options *CreateOptions) SerializeSeriesOptions(cmd string, args []interface{}) (result []interface{}, err error) {
138 | result = args
139 | if options.DuplicatePolicy != "" {
140 | if cmd == ADD_CMD {
141 | result = append(result, "ON_DUPLICATE", string(options.DuplicatePolicy))
142 | } else {
143 | result = append(result, "DUPLICATE_POLICY", string(options.DuplicatePolicy))
144 | }
145 | }
146 | return options.Serialize(result)
147 | }
148 |
149 | // Serialize options to args
150 | // Deprecated: This function has been deprecated given that DUPLICATE_POLICY and ON_DUPLICATE depend upon the issuing command, use SerializeSeriesOptions instead
151 | func (options *CreateOptions) Serialize(args []interface{}) (result []interface{}, err error) {
152 | result = args
153 | if options.Uncompressed {
154 | result = append(result, "UNCOMPRESSED")
155 | }
156 | if options.RetentionMSecs > 0 {
157 | var value int64
158 | err, value = formatMilliSec(options.RetentionMSecs)
159 | if err != nil {
160 | return
161 | }
162 | result = append(result, "RETENTION", value)
163 | }
164 | if options.ChunkSize > 0 {
165 | result = append(result, "CHUNK_SIZE", options.ChunkSize)
166 | }
167 | if len(options.Labels) > 0 {
168 | result = append(result, "LABELS")
169 | for key, value := range options.Labels {
170 | result = append(result, key, value)
171 | }
172 | }
173 | return
174 | }
175 |
176 | // Helper function to create a string pointer from a string literal.
177 | // Useful for calls to NewClient with an auth pass that is known at compile time.
178 | func MakeStringPtr(s string) *string {
179 | return &s
180 | }
181 |
182 | func floatToStr(inputFloat float64) string {
183 | return strconv.FormatFloat(inputFloat, 'g', 16, 64)
184 | }
185 |
186 | func strToFloat(inputString string) (float64, error) {
187 | return strconv.ParseFloat(inputString, 64)
188 | }
189 |
190 | func formatMilliSec(dur time.Duration) (error error, value int64) {
191 | if dur > 0 && dur < time.Millisecond {
192 | error = fmt.Errorf("specified duration is %s, but minimal supported value is %s", dur, time.Millisecond)
193 | return
194 | }
195 | value = int64(dur / time.Millisecond)
196 | return
197 | }
198 |
--------------------------------------------------------------------------------
/common_test.go:
--------------------------------------------------------------------------------
1 | package redis_timeseries_go
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 | "time"
7 | )
8 |
9 | func TestCreateOptions_Serialize(t *testing.T) {
10 | type fields struct {
11 | Uncompressed bool
12 | RetentionMSecs time.Duration
13 | Labels map[string]string
14 | ChunkSize int64
15 | DuplicatePolicy DuplicatePolicyType
16 | }
17 | type args struct {
18 | args []interface{}
19 | }
20 | tests := []struct {
21 | name string
22 | fields fields
23 | args args
24 | wantResult []interface{}
25 | wantErr bool
26 | }{
27 | {"empty", fields{false, 0, map[string]string{}, 0, ""}, args{[]interface{}{}}, []interface{}{}, false},
28 | {"UNCOMPRESSED", fields{true, 0, map[string]string{}, 0, ""}, args{[]interface{}{}}, []interface{}{"UNCOMPRESSED"}, false},
29 | {"RETENTION", fields{false, 1000000, map[string]string{}, 0, ""}, args{[]interface{}{}}, []interface{}{"RETENTION", int64(1)}, false},
30 | {"CHUNK_SIZE", fields{false, 1000000, map[string]string{}, 256, ""}, args{[]interface{}{}}, []interface{}{"RETENTION", int64(1), "CHUNK_SIZE", int64(256)}, false},
31 | }
32 | for _, tt := range tests {
33 | t.Run(tt.name, func(t *testing.T) {
34 | options := &CreateOptions{
35 | Uncompressed: tt.fields.Uncompressed,
36 | RetentionMSecs: tt.fields.RetentionMSecs,
37 | Labels: tt.fields.Labels,
38 | ChunkSize: tt.fields.ChunkSize,
39 | DuplicatePolicy: tt.fields.DuplicatePolicy,
40 | }
41 | gotResult, err := options.Serialize(tt.args.args)
42 | if (err != nil) != tt.wantErr {
43 | t.Errorf("Serialize() error = %v, wantErr %v", err, tt.wantErr)
44 | return
45 | }
46 | if !reflect.DeepEqual(gotResult, tt.wantResult) {
47 | t.Errorf("Serialize() gotResult = %v, want %v", gotResult, tt.wantResult)
48 | }
49 | })
50 | }
51 | }
52 |
53 | func Test_formatMilliSec(t *testing.T) {
54 | type args struct {
55 | dur time.Duration
56 | }
57 | tests := []struct {
58 | name string
59 | args args
60 | wantErr bool
61 | wantValue int64
62 | }{
63 | {"force error", args{1}, true, 0},
64 | {"Millisecond", args{time.Millisecond}, false, 1},
65 | {"Second", args{time.Second}, false, 1000},
66 | }
67 | for _, tt := range tests {
68 | t.Run(tt.name, func(t *testing.T) {
69 | err, gotValue := formatMilliSec(tt.args.dur)
70 | if (err != nil) != tt.wantErr {
71 | t.Errorf("formatMilliSec() error = %v, wantErr %v", err, tt.wantErr)
72 | return
73 | }
74 | if gotValue != tt.wantValue {
75 | t.Errorf("formatMilliSec() gotValue = %v, want %v", gotValue, tt.wantValue)
76 | }
77 | })
78 | }
79 | }
80 |
81 | func Test_strToFloat(t *testing.T) {
82 | type args struct {
83 | inputString string
84 | }
85 | tests := []struct {
86 | name string
87 | args args
88 | want float64
89 | wantErr bool
90 | }{
91 | {"2.0", args{"2.0"}, 2.0, false},
92 | }
93 | for _, tt := range tests {
94 | t.Run(tt.name, func(t *testing.T) {
95 | got, err := strToFloat(tt.args.inputString)
96 | if (err != nil) != tt.wantErr {
97 | t.Errorf("strToFloat() error = %v, wantErr %v", err, tt.wantErr)
98 | return
99 | }
100 | if got != tt.want {
101 | t.Errorf("strToFloat() got = %v, want %v", got, tt.want)
102 | }
103 | })
104 | }
105 | }
106 |
107 | func TestCreateOptions_SerializeSeriesOptions(t *testing.T) {
108 | type fields struct {
109 | Uncompressed bool
110 | RetentionMSecs time.Duration
111 | Labels map[string]string
112 | ChunkSize int64
113 | DuplicatePolicy DuplicatePolicyType
114 | }
115 | type args struct {
116 | cmd string
117 | args []interface{}
118 | }
119 | tests := []struct {
120 | name string
121 | fields fields
122 | args args
123 | wantResult []interface{}
124 | wantErr bool
125 | }{
126 | {"DUPLICATE_POLICY BLOCK", fields{false, 1000000, map[string]string{}, 256, BlockDuplicatePolicy}, args{"TS.CREATE", []interface{}{}}, []interface{}{"DUPLICATE_POLICY", "block", "RETENTION", int64(1), "CHUNK_SIZE", int64(256)}, false},
127 | {"DUPLICATE_POLICY FIRST", fields{false, 1000000, map[string]string{}, 256, FirstDuplicatePolicy}, args{"TS.CREATE", []interface{}{}}, []interface{}{"DUPLICATE_POLICY", "first", "RETENTION", int64(1), "CHUNK_SIZE", int64(256)}, false},
128 | {"DUPLICATE_POLICY LAST", fields{false, 1000000, map[string]string{}, 256, LastDuplicatePolicy}, args{"TS.CREATE", []interface{}{}}, []interface{}{"DUPLICATE_POLICY", "last", "RETENTION", int64(1), "CHUNK_SIZE", int64(256)}, false},
129 | {"DUPLICATE_POLICY MIN", fields{false, 1000000, map[string]string{}, 256, MinDuplicatePolicy}, args{"TS.CREATE", []interface{}{}}, []interface{}{"DUPLICATE_POLICY", "min", "RETENTION", int64(1), "CHUNK_SIZE", int64(256)}, false},
130 | {"DUPLICATE_POLICY MAX", fields{false, 1000000, map[string]string{}, 256, MaxDuplicatePolicy}, args{"TS.CREATE", []interface{}{}}, []interface{}{"DUPLICATE_POLICY", "max", "RETENTION", int64(1), "CHUNK_SIZE", int64(256)}, false},
131 | {"TS.ADD DUPLICATE_POLICY BLOCK", fields{false, 1000000, map[string]string{}, 256, BlockDuplicatePolicy}, args{"TS.ADD", []interface{}{}}, []interface{}{"ON_DUPLICATE", "block", "RETENTION", int64(1), "CHUNK_SIZE", int64(256)}, false},
132 | {"TS.ADD DUPLICATE_POLICY FIRST", fields{false, 1000000, map[string]string{}, 256, FirstDuplicatePolicy}, args{"TS.ADD", []interface{}{}}, []interface{}{"ON_DUPLICATE", "first", "RETENTION", int64(1), "CHUNK_SIZE", int64(256)}, false},
133 | {"TS.ADD DUPLICATE_POLICY LAST", fields{false, 1000000, map[string]string{}, 256, LastDuplicatePolicy}, args{"TS.ADD", []interface{}{}}, []interface{}{"ON_DUPLICATE", "last", "RETENTION", int64(1), "CHUNK_SIZE", int64(256)}, false},
134 | {"TS.ADD DUPLICATE_POLICY MIN", fields{false, 1000000, map[string]string{}, 256, MinDuplicatePolicy}, args{"TS.ADD", []interface{}{}}, []interface{}{"ON_DUPLICATE", "min", "RETENTION", int64(1), "CHUNK_SIZE", int64(256)}, false},
135 | {"TS.ADD DUPLICATE_POLICY MAX", fields{false, 1000000, map[string]string{}, 256, MaxDuplicatePolicy}, args{"TS.ADD", []interface{}{}}, []interface{}{"ON_DUPLICATE", "max", "RETENTION", int64(1), "CHUNK_SIZE", int64(256)}, false},
136 | }
137 | for _, tt := range tests {
138 | t.Run(tt.name, func(t *testing.T) {
139 | options := &CreateOptions{
140 | Uncompressed: tt.fields.Uncompressed,
141 | RetentionMSecs: tt.fields.RetentionMSecs,
142 | Labels: tt.fields.Labels,
143 | ChunkSize: tt.fields.ChunkSize,
144 | DuplicatePolicy: tt.fields.DuplicatePolicy,
145 | }
146 | gotResult, err := options.SerializeSeriesOptions(tt.args.cmd, tt.args.args)
147 | if (err != nil) != tt.wantErr {
148 | t.Errorf("SerializeSeriesOptions() error = %v, wantErr %v", err, tt.wantErr)
149 | return
150 | }
151 | if !reflect.DeepEqual(gotResult, tt.wantResult) {
152 | t.Errorf("SerializeSeriesOptions() gotResult = %v, want %v", gotResult, tt.wantResult)
153 | }
154 | })
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/example_client_test.go:
--------------------------------------------------------------------------------
1 | package redis_timeseries_go_test
2 |
3 | import (
4 | "fmt"
5 | redistimeseries "github.com/RedisTimeSeries/redistimeseries-go"
6 | "github.com/gomodule/redigo/redis"
7 | "log"
8 | "time"
9 | )
10 |
11 | // exemplifies the NewClientFromPool function
12 | //nolint:errcheck
13 | func ExampleNewClientFromPool() {
14 | host := "localhost:6379"
15 | password := ""
16 | pool := &redis.Pool{Dial: func() (redis.Conn, error) {
17 | return redis.Dial("tcp", host, redis.DialPassword(password))
18 | }}
19 | client := redistimeseries.NewClientFromPool(pool, "ts-client-1")
20 | client.Add("ts", 1, 5)
21 | datapoints, _ := client.RangeWithOptions("ts", 0, 1000, redistimeseries.DefaultRangeOptions)
22 | fmt.Println(datapoints[0])
23 | // Output: {1 5}
24 | }
25 |
26 | // Exemplifies the usage of CreateKeyWithOptions function with a duplicate policy of LAST (override with latest value)
27 | // nolint:errcheck
28 | func ExampleClient_CreateKeyWithOptions() {
29 | host := "localhost:6379"
30 | password := ""
31 | pool := &redis.Pool{Dial: func() (redis.Conn, error) {
32 | return redis.Dial("tcp", host, redis.DialPassword(password))
33 | }}
34 | client := redistimeseries.NewClientFromPool(pool, "ts-client-1")
35 |
36 | client.CreateKeyWithOptions("time-serie-last-policy", redistimeseries.CreateOptions{DuplicatePolicy: redistimeseries.LastDuplicatePolicy})
37 |
38 | // Add duplicate timestamp just to ensure it obeys the duplicate policy
39 | client.Add("time-serie-last-policy", 4, 2.0)
40 | client.Add("time-serie-last-policy", 4, 10.0)
41 |
42 | // Retrieve the latest data point
43 | latestDatapoint, _ := client.Get("time-serie-last-policy")
44 |
45 | fmt.Printf("Latest datapoint: timestamp=%d value=%f\n", latestDatapoint.Timestamp, latestDatapoint.Value)
46 | // Output:
47 | // Latest datapoint: timestamp=4 value=10.000000
48 | }
49 |
50 | // Exemplifies the usage of CreateKeyWithOptions function with a retention time of 1 hour
51 | // nolint:errcheck
52 | func ExampleClient_CreateKeyWithOptions_retentionTime() {
53 | host := "localhost:6379"
54 | password := ""
55 | pool := &redis.Pool{Dial: func() (redis.Conn, error) {
56 | return redis.Dial("tcp", host, redis.DialPassword(password))
57 | }}
58 | client := redistimeseries.NewClientFromPool(pool, "ts-client")
59 |
60 | // get the default options and set the retention time
61 | options := redistimeseries.DefaultCreateOptions
62 | options.RetentionMSecs = time.Hour
63 |
64 | client.CreateKeyWithOptions("time-series-example-retention-time", options)
65 |
66 | client.Add("time-series-example-retention-time", 1, 1)
67 | client.Add("time-series-example-retention-time", 2, 2)
68 |
69 | // Retrieve the latest data point
70 | latestDatapoint, _ := client.Get("time-series-example-retention-time")
71 |
72 | fmt.Printf("Latest datapoint: timestamp=%d value=%f\n", latestDatapoint.Timestamp, latestDatapoint.Value)
73 | // Output:
74 | // Latest datapoint: timestamp=2 value=2.000000
75 | }
76 |
77 | // Exemplifies the usage of Add function with a time-series created with the default options
78 | // nolint:errcheck
79 | func ExampleClient_Add() {
80 | host := "localhost:6379"
81 | password := ""
82 | pool := &redis.Pool{Dial: func() (redis.Conn, error) {
83 | return redis.Dial("tcp", host, redis.DialPassword(password))
84 | }}
85 | client := redistimeseries.NewClientFromPool(pool, "ts-client-1")
86 |
87 | labels := map[string]string{
88 | "machine": "machine-1",
89 | "az": "us-west-2",
90 | }
91 | // get the default options and set the time-serie labels
92 | options := redistimeseries.DefaultCreateOptions
93 | options.Labels = labels
94 |
95 | client.CreateKeyWithOptions("time-serie-add", options)
96 |
97 | client.Add("time-serie-add", 1, 2.0)
98 | client.Add("time-serie-add", 2, 4.0)
99 |
100 | // Retrieve the latest data point
101 | latestDatapoint, _ := client.Get("time-serie-add")
102 |
103 | fmt.Printf("Latest datapoint: timestamp=%d value=%f\n", latestDatapoint.Timestamp, latestDatapoint.Value)
104 | // Output:
105 | // Latest datapoint: timestamp=2 value=4.000000
106 | }
107 |
108 | // Exemplifies the usage of Add function for back filling - Add samples to a time series where the time of the sample is older than the newest sample in the series
109 | // nolint:errcheck
110 | func ExampleClient_Add_backFilling() {
111 | host := "localhost:6379"
112 | password := ""
113 | pool := &redis.Pool{Dial: func() (redis.Conn, error) {
114 | return redis.Dial("tcp", host, redis.DialPassword(password))
115 | }}
116 | client := redistimeseries.NewClientFromPool(pool, "ts-client-1")
117 |
118 | // get the default options and set the time-serie labels
119 | options := redistimeseries.DefaultCreateOptions
120 |
121 | client.CreateKeyWithOptions("time-serie-add-back-filling", options)
122 |
123 | client.Add("time-serie-add-back-filling", 1, 1)
124 | client.Add("time-serie-add-back-filling", 2, 1)
125 | client.Add("time-serie-add-back-filling", 4, 1)
126 | // Add sample with timestamp ( 3 ) where the time of the sample is older than the newest sample in the series ( 4 )
127 | client.Add("time-serie-add-back-filling", 3, 1)
128 |
129 | // Retrieve the time-series data points
130 | datapoints, _ := client.RangeWithOptions("time-serie-add-back-filling", 0, 1000, redistimeseries.DefaultRangeOptions)
131 | fmt.Printf("Datapoints: %v\n", datapoints)
132 | // Output:
133 | // Datapoints: [{1 1} {2 1} {3 1} {4 1}]
134 | }
135 |
136 | // Exemplifies the usage of Add function with a duplicate policy of LAST (override with latest value)
137 | // nolint:errcheck
138 | func ExampleClient_Add_duplicateDatapointsLastDuplicatePolicy() {
139 | host := "localhost:6379"
140 | password := ""
141 | pool := &redis.Pool{Dial: func() (redis.Conn, error) {
142 | return redis.Dial("tcp", host, redis.DialPassword(password))
143 | }}
144 | client := redistimeseries.NewClientFromPool(pool, "ts-client-1")
145 |
146 | // get the default options and set the duplicate policy to LAST (override with latest value)
147 | options := redistimeseries.DefaultCreateOptions
148 | options.DuplicatePolicy = redistimeseries.LastDuplicatePolicy
149 |
150 | client.CreateKeyWithOptions("time-series-add-duplicate-last", options)
151 |
152 | client.Add("time-series-add-duplicate-last", 1, 1.0)
153 | client.Add("time-series-add-duplicate-last", 1, 10.0)
154 |
155 | // Retrieve the latest data point
156 | latestDatapoint, _ := client.Get("time-series-add-duplicate-last")
157 |
158 | fmt.Printf("Latest datapoint: timestamp=%d value=%f\n", latestDatapoint.Timestamp, latestDatapoint.Value)
159 | // Output:
160 | // Latest datapoint: timestamp=1 value=10.000000
161 | }
162 |
163 | // Exemplifies the usage of Add function with a duplicate policy of MIN (override with min value)
164 | // nolint:errcheck
165 | func ExampleClient_Add_minDuplicatePolicy() {
166 | host := "localhost:6379"
167 | password := ""
168 | pool := &redis.Pool{Dial: func() (redis.Conn, error) {
169 | return redis.Dial("tcp", host, redis.DialPassword(password))
170 | }}
171 | client := redistimeseries.NewClientFromPool(pool, "ts-client-1")
172 |
173 | // get the default options and set the duplicate policy to MIN (override with latest value)
174 | options := redistimeseries.DefaultCreateOptions
175 | options.DuplicatePolicy = redistimeseries.MinDuplicatePolicy
176 |
177 | client.CreateKeyWithOptions("time-series-add-duplicate-min", options)
178 |
179 | client.Add("time-series-add-duplicate-min", 1, 1.0)
180 | client.Add("time-series-add-duplicate-min", 1, 10.0)
181 |
182 | // Retrieve the minimal data point
183 | minDatapoint, _ := client.Get("time-series-add-duplicate-min")
184 |
185 | fmt.Printf("Minimal datapoint: timestamp=%d value=%f\n", minDatapoint.Timestamp, minDatapoint.Value)
186 | // Output:
187 | // Minimal datapoint: timestamp=1 value=1.000000
188 | }
189 |
190 | // Exemplifies the usage of AddWithOptions function with the default options and some additional time-serie labels
191 | // nolint:errcheck
192 | func ExampleClient_AddWithOptions() {
193 | host := "localhost:6379"
194 | password := ""
195 | pool := &redis.Pool{Dial: func() (redis.Conn, error) {
196 | return redis.Dial("tcp", host, redis.DialPassword(password))
197 | }}
198 | client := redistimeseries.NewClientFromPool(pool, "ts-client")
199 |
200 | labels := map[string]string{
201 | "machine": "machine-1",
202 | "az": "us-west-2",
203 | }
204 | // get the default options and set the time-serie labels
205 | options := redistimeseries.DefaultCreateOptions
206 | options.Labels = labels
207 |
208 | client.AddWithOptions("time-series-example-add", 1, 1, options)
209 | client.AddWithOptions("time-series-example-add", 2, 2, options)
210 |
211 | // Retrieve the latest data point
212 | latestDatapoint, _ := client.Get("time-series-example-add")
213 |
214 | fmt.Printf("Latest datapoint: timestamp=%d value=%f\n", latestDatapoint.Timestamp, latestDatapoint.Value)
215 | // Output:
216 | // Latest datapoint: timestamp=2 value=2.000000
217 | }
218 |
219 | // Exemplifies the usage of AddWithOptions function with a duplicate policy of LAST (override with latest value)
220 | // and with MIN (override with minimal value)
221 | // nolint:errcheck
222 | func ExampleClient_AddWithOptions_duplicateDatapointsLastDuplicatePolicy() {
223 | host := "localhost:6379"
224 | password := ""
225 | pool := &redis.Pool{Dial: func() (redis.Conn, error) {
226 | return redis.Dial("tcp", host, redis.DialPassword(password))
227 | }}
228 | client := redistimeseries.NewClientFromPool(pool, "ts-client")
229 |
230 | labels := map[string]string{
231 | "machine": "machine-1",
232 | "az": "us-west-2",
233 | }
234 |
235 | // get the default options and set the duplicate policy to LAST (override with latest value)
236 | options := redistimeseries.DefaultCreateOptions
237 | options.DuplicatePolicy = redistimeseries.LastDuplicatePolicy
238 | options.Labels = labels
239 |
240 | client.AddWithOptions("time-series-example-duplicate", 1, 1, options)
241 | client.AddWithOptions("time-series-example-duplicate", 1, 10, options)
242 |
243 | // Retrieve the latest data point
244 | latestDatapoint, _ := client.Get("time-series-example-duplicate")
245 |
246 | // change the duplicate policy to MIN
247 | options.DuplicatePolicy = redistimeseries.MinDuplicatePolicy
248 |
249 | // The current value will not be overridden because the new added value is higher
250 | client.AddWithOptions("time-series-example-duplicate", 1, 15, options)
251 |
252 | // Retrieve the latest data point
253 | minDatapoint, _ := client.Get("time-series-example-duplicate")
254 |
255 | fmt.Printf("Latest datapoint: timestamp=%d value=%f\n", latestDatapoint.Timestamp, latestDatapoint.Value)
256 | fmt.Printf("Minimal datapoint: timestamp=%d value=%f\n", minDatapoint.Timestamp, minDatapoint.Value)
257 | // Output:
258 | // Latest datapoint: timestamp=1 value=10.000000
259 | // Minimal datapoint: timestamp=1 value=10.000000
260 | }
261 |
262 | // Exemplifies the usage of AddWithOptions function with a duplicate policy of MAX (only override if the value is higher than the existing value)
263 | // nolint:errcheck
264 | func ExampleClient_AddWithOptions_duplicateDatapointsMaxDuplicatePolicy() {
265 | host := "localhost:6379"
266 | password := ""
267 | pool := &redis.Pool{Dial: func() (redis.Conn, error) {
268 | return redis.Dial("tcp", host, redis.DialPassword(password))
269 | }}
270 | client := redistimeseries.NewClientFromPool(pool, "ts-client")
271 |
272 | labels := map[string]string{
273 | "machine": "machine-1",
274 | "az": "us-west-2",
275 | }
276 |
277 | // get the default options and set the duplicate policy to MAX (only override if the value is higher than the existing value)
278 | options := redistimeseries.DefaultCreateOptions
279 | options.DuplicatePolicy = redistimeseries.MaxDuplicatePolicy
280 | options.Labels = labels
281 |
282 | client.AddWithOptions("time-series-example-duplicate-max", 1, 10.0, options)
283 |
284 | // this should not override the value given that the previous one ( 10.0 ) is greater than the new one we're trying to add
285 | client.AddWithOptions("time-series-example-duplicate-max", 1, 5.0, options)
286 |
287 | // Retrieve the latest data point
288 | latestDatapoint, _ := client.Get("time-series-example-duplicate-max")
289 |
290 | fmt.Printf("Latest datapoint: timestamp=%d value=%f\n", latestDatapoint.Timestamp, latestDatapoint.Value)
291 | // Output:
292 | // Latest datapoint: timestamp=1 value=10.000000
293 | }
294 |
295 | // Exemplifies the usage of AddWithOptions function for back filling - Add samples to a time series where the time of the sample is older than the newest sample in the series
296 | // nolint:errcheck
297 | func ExampleClient_AddWithOptions_backFilling() {
298 | host := "localhost:6379"
299 | password := ""
300 | pool := &redis.Pool{Dial: func() (redis.Conn, error) {
301 | return redis.Dial("tcp", host, redis.DialPassword(password))
302 | }}
303 | client := redistimeseries.NewClientFromPool(pool, "ts-client")
304 |
305 | labels := map[string]string{
306 | "machine": "machine-1",
307 | "az": "us-west-2",
308 | }
309 |
310 | // use the default options
311 | options := redistimeseries.DefaultCreateOptions
312 | options.Labels = labels
313 |
314 | client.AddWithOptions("time-series-example-back-filling", 1, 1, options)
315 | client.AddWithOptions("time-series-example-back-filling", 2, 1, options)
316 | client.AddWithOptions("time-series-example-back-filling", 4, 1, options)
317 | // Add sample with timestamp ( 3 ) where the time of the sample is older than the newest sample in the series ( 4 )
318 | client.AddWithOptions("time-series-example-back-filling", 3, 1, options)
319 |
320 | // Retrieve the time-series data points
321 | datapoints, _ := client.RangeWithOptions("time-series-example-back-filling", 0, 1000, redistimeseries.DefaultRangeOptions)
322 | fmt.Printf("Datapoints: %v\n", datapoints)
323 | // Output:
324 | // Datapoints: [{1 1} {2 1} {3 1} {4 1}]
325 | }
326 |
327 | // Exemplifies the usage of RangeWithOptions function
328 | // nolint:errcheck
329 | func ExampleClient_RangeWithOptions() {
330 | host := "localhost:6379"
331 | password := ""
332 | pool := &redis.Pool{Dial: func() (redis.Conn, error) {
333 | return redis.Dial("tcp", host, redis.DialPassword(password))
334 | }}
335 | client := redistimeseries.NewClientFromPool(pool, "ts-client-1")
336 | for ts := 1; ts < 10; ts++ {
337 | client.Add("ts-1", int64(ts), float64(ts))
338 | }
339 |
340 | datapoints, _ := client.RangeWithOptions("ts-1", 0, 1000, redistimeseries.DefaultRangeOptions)
341 | fmt.Printf("Datapoints: %v\n", datapoints)
342 | // Output:
343 | // Datapoints: [{1 1} {2 2} {3 3} {4 4} {5 5} {6 6} {7 7} {8 8} {9 9}]
344 | }
345 |
346 | // Exemplifies the usage of RangeWithOptions function, while changing the reference timestamp on which a bucket is defined.
347 | // nolint:errcheck
348 | func ExampleClient_RangeWithOptions_aggregationMax() {
349 | host := "localhost:6379"
350 | password := ""
351 | pool := &redis.Pool{Dial: func() (redis.Conn, error) {
352 | return redis.Dial("tcp", host, redis.DialPassword(password))
353 | }}
354 | client := redistimeseries.NewClientFromPool(pool, "ts-client-1")
355 | for ts := 1; ts < 10; ts++ {
356 | client.Add("ts-1", int64(ts), float64(ts))
357 | }
358 |
359 | datapoints, _ := client.RangeWithOptions("ts-1", 0, 1000, *redistimeseries.NewRangeOptions().SetAggregation(redistimeseries.MaxAggregation, 5))
360 | fmt.Printf("Datapoints: %v\n", datapoints)
361 | // Output:
362 | // Datapoints: [{0 4} {5 9}]
363 | }
364 |
365 | // Exemplifies the usage of RangeWithOptions function, while changing the reference timestamp on which a bucket is defined.
366 | // nolint:errcheck
367 | func ExampleClient_RangeWithOptions_aggregationAlign() {
368 | host := "localhost:6379"
369 | password := ""
370 | pool := &redis.Pool{Dial: func() (redis.Conn, error) {
371 | return redis.Dial("tcp", host, redis.DialPassword(password))
372 | }}
373 | client := redistimeseries.NewClientFromPool(pool, "ts-client-1")
374 | for ts := 1; ts < 10; ts++ {
375 | client.Add("ts-1", int64(ts), float64(ts))
376 | }
377 |
378 | datapoints, _ := client.RangeWithOptions("ts-1", 0, 1000, *redistimeseries.NewRangeOptions().SetAggregation(redistimeseries.CountAggregation, 2).SetAlign(1))
379 | fmt.Printf("Datapoints: %v\n", datapoints)
380 | // Output:
381 | // Datapoints: [{1 2} {3 2} {5 2} {7 2} {9 1}]
382 | }
383 |
384 | // nolint
385 | // Exemplifies the usage of ReverseRangeWithOptions function
386 | func ExampleClient_ReverseRangeWithOptions() {
387 | host := "localhost:6379"
388 | password := ""
389 | pool := &redis.Pool{Dial: func() (redis.Conn, error) {
390 | return redis.Dial("tcp", host, redis.DialPassword(password))
391 | }}
392 | client := redistimeseries.NewClientFromPool(pool, "ts-client-1")
393 | for ts := 1; ts < 10; ts++ {
394 | client.Add("ts-2", int64(ts), float64(ts))
395 | }
396 |
397 | datapoints, _ := client.ReverseRangeWithOptions("ts-2", 0, 1000, redistimeseries.DefaultRangeOptions)
398 | fmt.Printf("Datapoints: %v\n", datapoints)
399 | // Output:
400 | // Datapoints: [{9 9} {8 8} {7 7} {6 6} {5 5} {4 4} {3 3} {2 2} {1 1}]
401 | }
402 |
403 | // nolint
404 | // Exemplifies the usage of ReverseRangeWithOptions function while filtering value
405 | func ExampleClient_ReverseRangeWithOptions_filterByValue() {
406 | host := "localhost:6379"
407 | password := ""
408 | pool := &redis.Pool{Dial: func() (redis.Conn, error) {
409 | return redis.Dial("tcp", host, redis.DialPassword(password))
410 | }}
411 | client := redistimeseries.NewClientFromPool(pool, "ts-client-1")
412 | for ts := 1; ts < 10; ts++ {
413 | client.Add("ts-2", int64(ts), float64(ts))
414 | }
415 |
416 | datapoints, _ := client.ReverseRangeWithOptions("ts-2", 0, 1000, *redistimeseries.NewRangeOptions().SetFilterByValue(5, 50))
417 | fmt.Printf("Datapoints: %v\n", datapoints)
418 | // Output:
419 | // Datapoints: [{9 9} {8 8} {7 7} {6 6} {5 5}]
420 | }
421 |
422 | // nolint
423 | // Exemplifies the usage of ReverseRangeWithOptions function while filtering by timestamp
424 | func ExampleClient_ReverseRangeWithOptions_filterByTs() {
425 | host := "localhost:6379"
426 | password := ""
427 | pool := &redis.Pool{Dial: func() (redis.Conn, error) {
428 | return redis.Dial("tcp", host, redis.DialPassword(password))
429 | }}
430 | client := redistimeseries.NewClientFromPool(pool, "ts-client-1")
431 | for ts := 1; ts < 10; ts++ {
432 | client.Add("ts-2", int64(ts), float64(ts))
433 | }
434 |
435 | datapoints, _ := client.ReverseRangeWithOptions("ts-2", 0, 1000, *redistimeseries.NewRangeOptions().SetFilterByTs([]int64{1, 2, 3, 4, 5}))
436 | fmt.Printf("Datapoints: %v\n", datapoints)
437 | // Output:
438 | // Datapoints: [{5 5} {4 4} {3 3} {2 2} {1 1}]
439 | }
440 |
441 | // nolint
442 | // Exemplifies the usage of MultiRangeWithOptions function.
443 | func ExampleClient_MultiRangeWithOptions() {
444 | host := "localhost:6379"
445 | password := ""
446 | pool := &redis.Pool{Dial: func() (redis.Conn, error) {
447 | return redis.Dial("tcp", host, redis.DialPassword(password))
448 | }}
449 | client := redistimeseries.NewClientFromPool(pool, "ts-client-1")
450 |
451 | // ensure clean DB
452 | client.FlushAll()
453 |
454 | labels1 := map[string]string{
455 | "machine": "machine-1",
456 | "az": "us-east-1",
457 | }
458 | client.AddWithOptions("time-serie-1", 2, 1.0, redistimeseries.CreateOptions{Labels: labels1})
459 | client.Add("time-serie-1", 4, 2.0)
460 |
461 | labels2 := map[string]string{
462 | "machine": "machine-2",
463 | "az": "us-east-1",
464 | }
465 | client.AddWithOptions("time-serie-2", 1, 5.0, redistimeseries.CreateOptions{Labels: labels2})
466 | client.Add("time-serie-2", 4, 10.0)
467 |
468 | ranges, _ := client.MultiRangeWithOptions(1, 10, redistimeseries.DefaultMultiRangeOptions, "az=us-east-1")
469 |
470 | fmt.Printf("Ranges: %v\n", ranges)
471 | // Output:
472 | // Ranges: [{time-serie-1 map[] [{2 1} {4 2}]} {time-serie-2 map[] [{1 5} {4 10}]}]
473 | }
474 |
475 | // nolint
476 | // Exemplifies the usage of MultiRangeWithOptions function.
477 | // grouping multiple time-series
478 | func ExampleClient_MultiRangeWithOptions_groupByReduce() {
479 | host := "localhost:6379"
480 | password := ""
481 | pool := &redis.Pool{Dial: func() (redis.Conn, error) {
482 | return redis.Dial("tcp", host, redis.DialPassword(password))
483 | }}
484 | client := redistimeseries.NewClientFromPool(pool, "ts-client-1")
485 |
486 | // ensure clean DB
487 | client.FlushAll()
488 |
489 | labels1 := map[string]string{
490 | "machine": "machine-1",
491 | "az": "us-east-1",
492 | "team": "team-1",
493 | }
494 | client.AddWithOptions("time-serie-1", 2, 1.0, redistimeseries.CreateOptions{Labels: labels1})
495 | client.Add("time-serie-1", 4, 2.0)
496 |
497 | labels2 := map[string]string{
498 | "machine": "machine-2",
499 | "az": "us-east-1",
500 | "team": "team-2",
501 | }
502 | client.AddWithOptions("time-serie-2", 1, 5.0, redistimeseries.CreateOptions{Labels: labels2})
503 | client.Add("time-serie-2", 4, 10.0)
504 |
505 | labels3 := map[string]string{
506 | "machine": "machine-3",
507 | "az": "us-east-1",
508 | "team": "team-2",
509 | }
510 | client.AddWithOptions("time-serie-3", 1, 55.0, redistimeseries.CreateOptions{Labels: labels3})
511 | client.Add("time-serie-3", 4, 99.0)
512 |
513 | // Find out the total resources usage by team
514 | ranges, _ := client.MultiRangeWithOptions(1, 10, *redistimeseries.NewMultiRangeOptions().SetWithLabels(true).SetGroupByReduce("team", redistimeseries.SumReducer), "az=us-east-1")
515 |
516 | fmt.Printf("Sum of usage by team: %v\n", ranges)
517 | // Output:
518 | // Sum of usage by team: [{team=team-1 map[__reducer__:sum __source__:time-serie-1 team:team-1] [{2 1} {4 2}]} {team=team-2 map[__reducer__:sum __source__:time-serie-2,time-serie-3 team:team-2] [{1 60} {4 109}]}]
519 | }
520 |
521 | // Exemplifies the usage of MultiRangeWithOptions function,
522 | // filtering the result by specific timestamps
523 | // nolint:errcheck
524 | func ExampleClient_MultiRangeWithOptions_filterByTs() {
525 | host := "localhost:6379"
526 | password := ""
527 | pool := &redis.Pool{Dial: func() (redis.Conn, error) {
528 | return redis.Dial("tcp", host, redis.DialPassword(password))
529 | }}
530 | client := redistimeseries.NewClientFromPool(pool, "ts-client-1")
531 |
532 | // ensure clean DB
533 | client.FlushAll()
534 |
535 | labels1 := map[string]string{
536 | "machine": "machine-1",
537 | "az": "us-east-1",
538 | }
539 | client.AddWithOptions("time-serie-1", 2, 1.0, redistimeseries.CreateOptions{Labels: labels1})
540 | client.Add("time-serie-1", 4, 2.0)
541 |
542 | labels2 := map[string]string{
543 | "machine": "machine-2",
544 | "az": "us-east-1",
545 | }
546 | client.AddWithOptions("time-serie-2", 1, 5.0, redistimeseries.CreateOptions{Labels: labels2})
547 | client.Add("time-serie-2", 4, 10.0)
548 |
549 | ranges, _ := client.MultiRangeWithOptions(1, 10, *redistimeseries.NewMultiRangeOptions().SetFilterByTs([]int64{1, 2}), "az=us-east-1")
550 |
551 | fmt.Printf("Ranges: %v\n", ranges)
552 | // Output:
553 | // Ranges: [{time-serie-1 map[] [{2 1}]} {time-serie-2 map[] [{1 5}]}]
554 | }
555 |
556 | // Exemplifies the usage of MultiRangeWithOptions function,
557 | // filtering the result by value using minimum and maximum.
558 | // nolint:errcheck
559 | func ExampleClient_MultiRangeWithOptions_filterByValue() {
560 | host := "localhost:6379"
561 | password := ""
562 | pool := &redis.Pool{Dial: func() (redis.Conn, error) {
563 | return redis.Dial("tcp", host, redis.DialPassword(password))
564 | }}
565 | client := redistimeseries.NewClientFromPool(pool, "ts-client-1")
566 |
567 | // ensure the DB is empty
568 | client.FlushAll()
569 |
570 | labels1 := map[string]string{
571 | "machine": "machine-1",
572 | "az": "us-east-1",
573 | }
574 | client.AddWithOptions("time-serie-1", 2, 1.0, redistimeseries.CreateOptions{Labels: labels1})
575 | client.Add("time-serie-1", 4, 2.0)
576 |
577 | labels2 := map[string]string{
578 | "machine": "machine-2",
579 | "az": "us-east-1",
580 | }
581 | client.AddWithOptions("time-serie-2", 1, 2.0, redistimeseries.CreateOptions{Labels: labels2})
582 | client.Add("time-serie-2", 4, 10.0)
583 |
584 | ranges, _ := client.MultiRangeWithOptions(1, 10, *redistimeseries.NewMultiRangeOptions().SetFilterByValue(1, 5), "az=us-east-1")
585 |
586 | fmt.Printf("Ranges: %v\n", ranges)
587 | // Output:
588 | // Ranges: [{time-serie-1 map[] [{2 1} {4 2}]} {time-serie-2 map[] [{1 2}]}]
589 | }
590 |
591 | // Exemplifies the usage of MultiRangeWithOptions function,
592 | // filtering the returned labels.
593 | // nolint:errcheck
594 | func ExampleClient_MultiRangeWithOptions_selectedLabels() {
595 | host := "localhost:6379"
596 | password := ""
597 | pool := &redis.Pool{Dial: func() (redis.Conn, error) {
598 | return redis.Dial("tcp", host, redis.DialPassword(password))
599 | }}
600 | client := redistimeseries.NewClientFromPool(pool, "ts-client-1")
601 |
602 | // ensure the DB is empty
603 | client.FlushAll()
604 |
605 | labels1 := map[string]string{
606 | "machine": "machine-1",
607 | "team": "SF-1",
608 | "location": "SF",
609 | "az": "us-east-1",
610 | }
611 | client.AddWithOptions("selected-labels-ex-time-serie-1", 2, 1.0, redistimeseries.CreateOptions{Labels: labels1})
612 | client.Add("selected-labels-ex-time-serie-1", 4, 2.0)
613 |
614 | labels2 := map[string]string{
615 | "machine": "machine-2",
616 | "team": "NY-1",
617 | "location": "NY",
618 | "az": "us-east-1",
619 | }
620 | client.AddWithOptions("selected-labels-ex-time-serie-2", 1, 10.0, redistimeseries.CreateOptions{Labels: labels2})
621 | client.Add("selected-labels-ex-time-serie-2", 4, 15.0)
622 |
623 | ranges, _ := client.MultiRangeWithOptions(1, 10, *redistimeseries.NewMultiRangeOptions().SetSelectedLabels([]string{"az", "location"}), "az=us-east-1")
624 |
625 | fmt.Printf("Ranges: %v\n", ranges)
626 | // Output:
627 | // Ranges: [{selected-labels-ex-time-serie-1 map[az:us-east-1 location:SF] [{2 1} {4 2}]} {selected-labels-ex-time-serie-2 map[az:us-east-1 location:NY] [{1 10} {4 15}]}]
628 | }
629 |
630 | // Exemplifies the usage of MultiReverseRangeWithOptions function.
631 | // nolint:errcheck
632 | func ExampleClient_MultiReverseRangeWithOptions() {
633 | host := "localhost:6379"
634 | password := ""
635 | pool := &redis.Pool{Dial: func() (redis.Conn, error) {
636 | return redis.Dial("tcp", host, redis.DialPassword(password))
637 | }}
638 | client := redistimeseries.NewClientFromPool(pool, "ts-client-1")
639 |
640 | // ensure the DB is empty
641 | client.FlushAll()
642 |
643 | labels1 := map[string]string{
644 | "machine": "machine-1",
645 | "az": "us-east-1",
646 | }
647 | client.AddWithOptions("time-serie-1", 2, 1.0, redistimeseries.CreateOptions{Labels: labels1})
648 | client.Add("time-serie-1", 4, 2.0)
649 |
650 | labels2 := map[string]string{
651 | "machine": "machine-2",
652 | "az": "us-east-1",
653 | }
654 | client.AddWithOptions("time-serie-2", 1, 5.0, redistimeseries.CreateOptions{Labels: labels2})
655 | client.Add("time-serie-2", 4, 10.0)
656 |
657 | ranges, _ := client.MultiReverseRangeWithOptions(1, 10, redistimeseries.DefaultMultiRangeOptions, "az=us-east-1")
658 |
659 | fmt.Printf("Ranges: %v\n", ranges)
660 | // Output:
661 | // Ranges: [{time-serie-1 map[] [{4 2} {2 1}]} {time-serie-2 map[] [{4 10} {1 5}]}]
662 | }
663 |
664 | // Exemplifies the usage of MultiReverseRangeWithOptions function,
665 | // filtering the result by specific timestamps
666 | // nolint:errcheck
667 | func ExampleClient_MultiReverseRangeWithOptions_filterByTs() {
668 | host := "localhost:6379"
669 | password := ""
670 | pool := &redis.Pool{Dial: func() (redis.Conn, error) {
671 | return redis.Dial("tcp", host, redis.DialPassword(password))
672 | }}
673 | client := redistimeseries.NewClientFromPool(pool, "ts-client-1")
674 |
675 | labels1 := map[string]string{
676 | "machine": "machine-1",
677 | "az": "us-east-1",
678 | }
679 | client.AddWithOptions("time-serie-1", 2, 1.0, redistimeseries.CreateOptions{Labels: labels1})
680 | client.Add("time-serie-1", 4, 2.0)
681 |
682 | labels2 := map[string]string{
683 | "machine": "machine-2",
684 | "az": "us-east-1",
685 | }
686 | client.AddWithOptions("time-serie-2", 1, 5.0, redistimeseries.CreateOptions{Labels: labels2})
687 | client.Add("time-serie-2", 4, 10.0)
688 |
689 | ranges, _ := client.MultiReverseRangeWithOptions(1, 10, *redistimeseries.NewMultiRangeOptions().SetFilterByTs([]int64{1, 2}), "az=us-east-1")
690 |
691 | fmt.Printf("Ranges: %v\n", ranges)
692 | // Output:
693 | // Ranges: [{time-serie-1 map[] [{2 1}]} {time-serie-2 map[] [{1 5}]}]
694 | }
695 |
696 | // Exemplifies the usage of MultiReverseRangeWithOptions function,
697 | // filtering the result by value using minimum and maximum.
698 | // nolint:errcheck
699 | func ExampleClient_MultiReverseRangeWithOptions_filterByValue() {
700 | host := "localhost:6379"
701 | password := ""
702 | pool := &redis.Pool{Dial: func() (redis.Conn, error) {
703 | return redis.Dial("tcp", host, redis.DialPassword(password))
704 | }}
705 | client := redistimeseries.NewClientFromPool(pool, "ts-client-1")
706 |
707 | // ensure the DB is empty
708 | client.FlushAll()
709 |
710 | labels1 := map[string]string{
711 | "machine": "machine-1",
712 | "az": "us-east-1",
713 | }
714 | client.AddWithOptions("time-serie-1", 2, 1.0, redistimeseries.CreateOptions{Labels: labels1})
715 | client.Add("time-serie-1", 4, 2.0)
716 |
717 | labels2 := map[string]string{
718 | "machine": "machine-2",
719 | "az": "us-east-1",
720 | }
721 | client.AddWithOptions("time-serie-2", 1, 2.0, redistimeseries.CreateOptions{Labels: labels2})
722 | client.Add("time-serie-2", 4, 10.0)
723 |
724 | ranges, _ := client.MultiReverseRangeWithOptions(1, 10, *redistimeseries.NewMultiRangeOptions().SetFilterByValue(1, 5), "az=us-east-1")
725 |
726 | fmt.Printf("Ranges: %v\n", ranges)
727 | // Output:
728 | // Ranges: [{time-serie-1 map[] [{4 2} {2 1}]} {time-serie-2 map[] [{1 2}]}]
729 | }
730 |
731 | // Exemplifies the usage of MultiReverseRangeWithOptions function,
732 | // filtering the returned labels.
733 | // nolint:errcheck
734 | func ExampleClient_MultiReverseRangeWithOptions_selectedLabels() {
735 | host := "localhost:6379"
736 | password := ""
737 | pool := &redis.Pool{Dial: func() (redis.Conn, error) {
738 | return redis.Dial("tcp", host, redis.DialPassword(password))
739 | }}
740 | client := redistimeseries.NewClientFromPool(pool, "ts-client-1")
741 |
742 | // ensure the DB is empty
743 | client.FlushAll()
744 |
745 | labels1 := map[string]string{
746 | "machine": "machine-1",
747 | "team": "SF-1",
748 | "location": "SF",
749 | "az": "us-east-1",
750 | }
751 | client.AddWithOptions("selected-labels-ex-time-serie-1", 2, 1.0, redistimeseries.CreateOptions{Labels: labels1})
752 | client.Add("selected-labels-ex-time-serie-1", 4, 2.0)
753 |
754 | labels2 := map[string]string{
755 | "machine": "machine-2",
756 | "team": "NY-1",
757 | "location": "NY",
758 | "az": "us-east-1",
759 | }
760 | client.AddWithOptions("selected-labels-ex-time-serie-2", 1, 10.0, redistimeseries.CreateOptions{Labels: labels2})
761 | client.Add("selected-labels-ex-time-serie-2", 4, 15.0)
762 |
763 | ranges, _ := client.MultiReverseRangeWithOptions(1, 10, *redistimeseries.NewMultiRangeOptions().SetSelectedLabels([]string{"az", "location"}), "az=us-east-1")
764 |
765 | fmt.Printf("Ranges: %v\n", ranges)
766 | // Output:
767 | // Ranges: [{selected-labels-ex-time-serie-1 map[az:us-east-1 location:SF] [{4 2} {2 1}]} {selected-labels-ex-time-serie-2 map[az:us-east-1 location:NY] [{4 15} {1 10}]}]
768 | }
769 |
770 | //nolint:errcheck
771 | // Exemplifies the usage of MultiGetWithOptions function while using the default MultiGetOptions and while using user defined MultiGetOptions.
772 | func ExampleClient_MultiGetWithOptions() {
773 | host := "localhost:6379"
774 | password := ""
775 | pool := &redis.Pool{Dial: func() (redis.Conn, error) {
776 | return redis.Dial("tcp", host, redis.DialPassword(password))
777 | }}
778 | client := redistimeseries.NewClientFromPool(pool, "ts-client-1")
779 |
780 | // ensure the DB is empty
781 | client.FlushAll()
782 |
783 | labels1 := map[string]string{
784 | "machine": "machine-1",
785 | "az": "us-east-1",
786 | }
787 | client.AddWithOptions("time-serie-1", 2, 1.0, redistimeseries.CreateOptions{Labels: labels1})
788 | client.Add("time-serie-1", 4, 2.0)
789 |
790 | labels2 := map[string]string{
791 | "machine": "machine-2",
792 | "az": "us-east-1",
793 | }
794 | client.AddWithOptions("time-serie-2", 1, 5.0, redistimeseries.CreateOptions{Labels: labels2})
795 | client.Add("time-serie-2", 4, 10.0)
796 |
797 | ranges, _ := client.MultiGetWithOptions(redistimeseries.DefaultMultiGetOptions, "az=us-east-1")
798 |
799 | rangesWithLabels, _ := client.MultiGetWithOptions(*redistimeseries.NewMultiGetOptions().SetWithLabels(true), "az=us-east-1")
800 |
801 | fmt.Printf("Ranges: %v\n", ranges)
802 | fmt.Printf("Ranges with labels: %v\n", rangesWithLabels)
803 |
804 | // Output:
805 | // Ranges: [{time-serie-1 map[] [{4 2}]} {time-serie-2 map[] [{4 10}]}]
806 | // Ranges with labels: [{time-serie-1 map[az:us-east-1 machine:machine-1] [{4 2}]} {time-serie-2 map[az:us-east-1 machine:machine-2] [{4 10}]}]
807 | }
808 |
809 | // Exemplifies the usage of MultiAdd.
810 | func ExampleClient_MultiAdd() {
811 | host := "localhost:6379"
812 | password := ""
813 | pool := &redis.Pool{Dial: func() (redis.Conn, error) {
814 | return redis.Dial("tcp", host, redis.DialPassword(password))
815 | }}
816 | client := redistimeseries.NewClientFromPool(pool, "ts-client-1")
817 |
818 | labels1 := map[string]string{
819 | "machine": "machine-1",
820 | "az": "us-east-1",
821 | }
822 | labels2 := map[string]string{
823 | "machine": "machine-2",
824 | "az": "us-east-1",
825 | }
826 |
827 | err := client.CreateKeyWithOptions("timeserie-1", redistimeseries.CreateOptions{Labels: labels1})
828 | if err != nil {
829 | log.Fatal(err)
830 | }
831 | err = client.CreateKeyWithOptions("timeserie-2", redistimeseries.CreateOptions{Labels: labels2})
832 | if err != nil {
833 | log.Fatal(err)
834 | }
835 |
836 | // Adding multiple datapoints to multiple series
837 | datapoints := []redistimeseries.Sample{
838 | {"timeserie-1", redistimeseries.DataPoint{1, 10.5}},
839 | {"timeserie-1", redistimeseries.DataPoint{2, 40.5}},
840 | {"timeserie-2", redistimeseries.DataPoint{1, 60.5}},
841 | }
842 | timestamps, _ := client.MultiAdd(datapoints...)
843 |
844 | fmt.Printf("Example adding multiple datapoints to multiple series. Added timestamps: %v\n", timestamps)
845 |
846 | // Adding multiple datapoints to the same serie
847 | datapointsSameSerie := []redistimeseries.Sample{
848 | {"timeserie-1", redistimeseries.DataPoint{3, 10.5}},
849 | {"timeserie-1", redistimeseries.DataPoint{4, 40.5}},
850 | {"timeserie-1", redistimeseries.DataPoint{5, 60.5}},
851 | }
852 | timestampsSameSerie, _ := client.MultiAdd(datapointsSameSerie...)
853 |
854 | fmt.Printf("Example of adding multiple datapoints to the same serie. Added timestamps: %v\n", timestampsSameSerie)
855 |
856 | // Output:
857 | // Example adding multiple datapoints to multiple series. Added timestamps: [1 2 1]
858 | // Example of adding multiple datapoints to the same serie. Added timestamps: [3 4 5]
859 | }
860 |
861 | // exemplifies the usage of DeleteSerie function
862 | //nolint:errcheck
863 | func ExampleClient_DeleteSerie() {
864 | host := "localhost:6379"
865 | password := ""
866 | pool := &redis.Pool{Dial: func() (redis.Conn, error) {
867 | return redis.Dial("tcp", host, redis.DialPassword(password))
868 | }}
869 | client := redistimeseries.NewClientFromPool(pool, "ts-client-1")
870 |
871 | // Create serie and add datapoint
872 | client.Add("ts", 1, 5)
873 |
874 | // Query the serie
875 | datapoints, _ := client.RangeWithOptions("ts", 0, 1000, redistimeseries.DefaultRangeOptions)
876 | fmt.Println(datapoints[0])
877 | // Output: {1 5}
878 |
879 | // Delete the serie
880 | client.DeleteSerie("ts")
881 |
882 | }
883 |
884 | // exemplifies the usage of DeleteRange function
885 | //nolint:errcheck
886 | func ExampleClient_DeleteRange() {
887 | host := "localhost:6379"
888 | password := ""
889 | pool := &redis.Pool{Dial: func() (redis.Conn, error) {
890 | return redis.Dial("tcp", host, redis.DialPassword(password))
891 | }}
892 | client := redistimeseries.NewClientFromPool(pool, "ts-client-1")
893 |
894 | // Create serie and add datapoint
895 | client.Add("ts", 1, 5)
896 | client.Add("ts", 10, 15.5)
897 | client.Add("ts", 20, 25)
898 |
899 | // Query the serie
900 | datapoints, _ := client.RangeWithOptions("ts", redistimeseries.TimeRangeMinimum, redistimeseries.TimeRangeMaximum, redistimeseries.DefaultRangeOptions)
901 | fmt.Println("Before deleting datapoints: ", datapoints)
902 |
903 | // Delete datapoints from timestamp 1 until 10 ( inclusive )
904 | totalDeletedSamples, _ := client.DeleteRange("ts", 1, 10)
905 | fmt.Printf("Deleted %d datapoints\n", totalDeletedSamples)
906 |
907 | // Query the serie after deleting from timestamp 1 until 10 ( inclusive )
908 | datapoints, _ = client.RangeWithOptions("ts", redistimeseries.TimeRangeMinimum, redistimeseries.TimeRangeMaximum, redistimeseries.DefaultRangeOptions)
909 | fmt.Println("After deleting datapoints: ", datapoints)
910 |
911 | // Output: Before deleting datapoints: [{1 5} {10 15.5} {20 25}]
912 | // Deleted 2 datapoints
913 | // After deleting datapoints: [{20 25}]
914 |
915 | }
916 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/RedisTimeSeries/redistimeseries-go
2 |
3 | go 1.12
4 |
5 | require (
6 | github.com/gomodule/redigo v1.8.2
7 | github.com/google/go-cmp v0.5.1
8 | github.com/stretchr/testify v1.6.1
9 | )
10 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3 | github.com/gomodule/redigo v1.8.2 h1:H5XSIre1MB5NbPYFp+i1NBbb5qN1W8Y8YAQoAYbkm8k=
4 | github.com/gomodule/redigo v1.8.2/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0=
5 | github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k=
6 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
7 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
8 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
9 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
10 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
11 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
12 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
13 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
14 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
15 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
16 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
17 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
18 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
19 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
20 |
--------------------------------------------------------------------------------
/multiget.go:
--------------------------------------------------------------------------------
1 | package redis_timeseries_go
2 |
3 | // MultiGetOptions represent the options for querying across multiple time-series
4 | type MultiGetOptions struct {
5 | WithLabels bool
6 | }
7 |
8 | // MultiGetOptions are the default options for querying across multiple time-series
9 | var DefaultMultiGetOptions = MultiGetOptions{
10 | WithLabels: false,
11 | }
12 |
13 | func NewMultiGetOptions() *MultiGetOptions {
14 | return &MultiGetOptions{
15 | WithLabels: false,
16 | }
17 | }
18 |
19 | func (mgetopts *MultiGetOptions) SetWithLabels(value bool) *MultiGetOptions {
20 | mgetopts.WithLabels = value
21 | return mgetopts
22 | }
23 |
24 | func createMultiGetCmdArguments(mgetOptions MultiGetOptions, filters []string) []interface{} {
25 | args := []interface{}{}
26 | if mgetOptions.WithLabels {
27 | args = append(args, "WITHLABELS")
28 | }
29 | args = append(args, "FILTER")
30 | for _, filter := range filters {
31 | args = append(args, filter)
32 | }
33 | return args
34 | }
35 |
--------------------------------------------------------------------------------
/multiget_test.go:
--------------------------------------------------------------------------------
1 | package redis_timeseries_go
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 | )
7 |
8 | func Test_createMultiGetCmdArguments(t *testing.T) {
9 | type args struct {
10 | mgetOptions MultiGetOptions
11 | filters []string
12 | }
13 | tests := []struct {
14 | name string
15 | args args
16 | want []interface{}
17 | }{
18 | {"default", args{DefaultMultiGetOptions, []string{"labels!="}}, []interface{}{"FILTER", "labels!="}},
19 | {"withlabels", args{*(NewMultiGetOptions().SetWithLabels(true)), []string{"labels!="}}, []interface{}{"WITHLABELS", "FILTER", "labels!="}},
20 | }
21 | for _, tt := range tests {
22 | t.Run(tt.name, func(t *testing.T) {
23 | if got := createMultiGetCmdArguments(tt.args.mgetOptions, tt.args.filters); !reflect.DeepEqual(got, tt.want) {
24 | t.Errorf("createMultiGetCmdArguments() = %v, want %v", got, tt.want)
25 | }
26 | })
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/multirange.go:
--------------------------------------------------------------------------------
1 | package redis_timeseries_go
2 |
3 | import (
4 | "fmt"
5 | "strconv"
6 | )
7 |
8 | // MultiRangeOptions represent the options for querying across multiple time-series
9 | type MultiRangeOptions struct {
10 | AggType AggregationType
11 | TimeBucket int
12 | Count int64
13 | WithLabels bool
14 | SelectedLabels []string
15 | Align int64
16 | FilterByTs []int64
17 | FilterByValueMin *float64
18 | FilterByValueMax *float64
19 | GroupBy string
20 | Reduce ReducerType
21 | }
22 |
23 | // MultiRangeOptions are the default options for querying across multiple time-series
24 | var DefaultMultiRangeOptions = MultiRangeOptions{
25 | AggType: "",
26 | TimeBucket: -1,
27 | Count: -1,
28 | WithLabels: false,
29 | SelectedLabels: []string{},
30 | Align: -1,
31 | FilterByTs: []int64{},
32 | FilterByValueMin: nil,
33 | FilterByValueMax: nil,
34 | GroupBy: "",
35 | Reduce: "",
36 | }
37 |
38 | func NewMultiRangeOptions() *MultiRangeOptions {
39 | return &MultiRangeOptions{
40 | AggType: "",
41 | TimeBucket: -1,
42 | Count: -1,
43 | WithLabels: false,
44 | SelectedLabels: []string{},
45 | Align: -1,
46 | FilterByTs: []int64{},
47 | FilterByValueMin: nil,
48 | FilterByValueMax: nil,
49 | GroupBy: "",
50 | Reduce: "",
51 | }
52 | }
53 |
54 | // SetGroupByReduce Aggregates results across different time series, grouped by the provided label name.
55 | // When combined with AGGREGATION the groupby/reduce is applied post aggregation stage.
56 | func (mrangeopts *MultiRangeOptions) SetGroupByReduce(byLabel string, reducer ReducerType) *MultiRangeOptions {
57 | mrangeopts.GroupBy = byLabel
58 | mrangeopts.Reduce = reducer
59 | return mrangeopts
60 | }
61 |
62 | // SetAlign sets the time bucket alignment control for AGGREGATION.
63 | // This will control the time bucket timestamps by changing the reference timestamp on which a bucket is defined.
64 | func (mrangeopts *MultiRangeOptions) SetAlign(byTimeStamp int64) *MultiRangeOptions {
65 | mrangeopts.Align = byTimeStamp
66 | return mrangeopts
67 | }
68 |
69 | // SetFilterByTs sets the list of timestamps to filter the result by specific timestamps
70 | func (mrangeopts *MultiRangeOptions) SetFilterByTs(filterByTS []int64) *MultiRangeOptions {
71 | mrangeopts.FilterByTs = filterByTS
72 | return mrangeopts
73 | }
74 |
75 | // SetFilterByValue filters the result by value using minimum and maximum ( inclusive )
76 | func (mrangeopts *MultiRangeOptions) SetFilterByValue(min, max float64) *MultiRangeOptions {
77 | mrangeopts.FilterByValueMin = &min
78 | mrangeopts.FilterByValueMax = &max
79 | return mrangeopts
80 | }
81 |
82 | func (mrangeopts *MultiRangeOptions) SetCount(count int64) *MultiRangeOptions {
83 | mrangeopts.Count = count
84 | return mrangeopts
85 | }
86 |
87 | func (mrangeopts *MultiRangeOptions) SetAggregation(aggType AggregationType, timeBucket int) *MultiRangeOptions {
88 | mrangeopts.AggType = aggType
89 | mrangeopts.TimeBucket = timeBucket
90 | return mrangeopts
91 | }
92 |
93 | func (mrangeopts *MultiRangeOptions) SetWithLabels(value bool) *MultiRangeOptions {
94 | mrangeopts.WithLabels = value
95 | return mrangeopts
96 | }
97 |
98 | // SetSelectedLabels limits the series reply labels to provided label names
99 | func (mrangeopts *MultiRangeOptions) SetSelectedLabels(labels []string) *MultiRangeOptions {
100 | mrangeopts.SelectedLabels = labels
101 | return mrangeopts
102 | }
103 |
104 | func createMultiRangeCmdArguments(fromTimestamp int64, toTimestamp int64, mrangeOptions MultiRangeOptions, filters []string) []interface{} {
105 | args := []interface{}{strconv.FormatInt(fromTimestamp, 10), strconv.FormatInt(toTimestamp, 10)}
106 | if mrangeOptions.FilterByValueMin != nil {
107 | args = append(args, "FILTER_BY_VALUE",
108 | fmt.Sprintf("%f", *mrangeOptions.FilterByValueMin),
109 | fmt.Sprintf("%f", *mrangeOptions.FilterByValueMax))
110 | }
111 | if len(mrangeOptions.FilterByTs) > 0 {
112 | args = append(args, "FILTER_BY_TS")
113 | for _, timestamp := range mrangeOptions.FilterByTs {
114 | args = append(args, strconv.FormatInt(timestamp, 10))
115 | }
116 | }
117 | if mrangeOptions.AggType != "" {
118 | args = append(args, "AGGREGATION", mrangeOptions.AggType, strconv.Itoa(mrangeOptions.TimeBucket))
119 | }
120 | if mrangeOptions.Count != -1 {
121 | args = append(args, "COUNT", strconv.FormatInt(mrangeOptions.Count, 10))
122 | }
123 | if mrangeOptions.WithLabels {
124 | args = append(args, "WITHLABELS")
125 | } else if len(mrangeOptions.SelectedLabels) > 0 {
126 | args = append(args, "SELECTED_LABELS")
127 | for _, label := range mrangeOptions.SelectedLabels {
128 | args = append(args, label)
129 | }
130 | }
131 | if mrangeOptions.Align != -1 {
132 | args = append(args, "ALIGN", strconv.FormatInt(mrangeOptions.Align, 10))
133 | }
134 | args = append(args, "FILTER")
135 | for _, filter := range filters {
136 | args = append(args, filter)
137 | }
138 | if mrangeOptions.GroupBy != "" {
139 | args = append(args, "GROUPBY", mrangeOptions.GroupBy, "REDUCE", string(mrangeOptions.Reduce))
140 | }
141 | return args
142 | }
143 |
--------------------------------------------------------------------------------
/multirange_test.go:
--------------------------------------------------------------------------------
1 | package redis_timeseries_go
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 | )
7 |
8 | func TestCreateMultiRangeCmdArguments(t *testing.T) {
9 | type args struct {
10 | fromTimestamp int64
11 | toTimestamp int64
12 | mrangeOptions MultiRangeOptions
13 | filters []string
14 | }
15 | tests := []struct {
16 | name string
17 | args args
18 | want []interface{}
19 | }{
20 | {"default",
21 | args{0, 1, DefaultMultiRangeOptions, []string{"labels!="}},
22 | []interface{}{"0", "1", "FILTER", "labels!="}},
23 | {"withlabels",
24 | args{0, 1, *(NewMultiRangeOptions().SetWithLabels(true)),
25 | []string{"labels!="}},
26 | []interface{}{"0", "1", "WITHLABELS", "FILTER", "labels!="}},
27 | {"withlabels and aggregation",
28 | args{0, 1, *(NewMultiRangeOptions().SetAggregation(AvgAggregation, 60).SetWithLabels(true)),
29 | []string{"labels!="}},
30 | []interface{}{"0", "1", "AGGREGATION", AvgAggregation, "60", "WITHLABELS", "FILTER", "labels!="}},
31 | {"withlabels, aggregation and count",
32 | args{0, 1, *(NewMultiRangeOptions().SetAggregation(AvgAggregation, 60).SetWithLabels(true).SetCount(120)),
33 | []string{"labels!="}},
34 | []interface{}{"0", "1", "AGGREGATION", AvgAggregation, "60", "COUNT", "120", "WITHLABELS", "FILTER", "labels!="}},
35 | {"withlabels, aggregation, count, and align",
36 | args{0, 1, *(NewMultiRangeOptions().SetAggregation(AvgAggregation, 60).SetWithLabels(true).SetCount(120).SetAlign(10)),
37 | []string{"labels!="}},
38 | []interface{}{"0", "1", "AGGREGATION", AvgAggregation, "60", "COUNT", "120", "WITHLABELS", "ALIGN", "10", "FILTER", "labels!="}},
39 | {"withlabels, aggregation, count, and align, filter by ts",
40 | args{0, 1, *(NewMultiRangeOptions().SetAggregation(AvgAggregation, 60).SetWithLabels(true).SetCount(120).SetAlign(10).SetFilterByTs([]int64{10, 11, 12, 13})),
41 | []string{"labels!="}},
42 | []interface{}{"0", "1", "FILTER_BY_TS", "10", "11", "12", "13", "AGGREGATION", AvgAggregation, "60", "COUNT", "120", "WITHLABELS", "ALIGN", "10", "FILTER", "labels!="}},
43 | {"withlabels, aggregation, count, and align, filter by value",
44 | args{0, 1, *(NewMultiRangeOptions().SetAggregation(AvgAggregation, 60).SetWithLabels(true).SetCount(120).SetAlign(10).SetFilterByValue(10, 13)),
45 | []string{"labels!="}},
46 | []interface{}{"0", "1", "FILTER_BY_VALUE", "10.000000", "13.000000", "AGGREGATION", AvgAggregation, "60", "COUNT", "120", "WITHLABELS", "ALIGN", "10", "FILTER", "labels!="}},
47 | {"selected_labels, aggregation, count, and align, filter by value",
48 | args{0, 1, *(NewMultiRangeOptions().SetAggregation(AvgAggregation, 60).SetSelectedLabels([]string{"l1", "l2"}).SetCount(120).SetAlign(10).SetFilterByValue(10, 13)),
49 | []string{"labels!="}},
50 | []interface{}{"0", "1", "FILTER_BY_VALUE", "10.000000", "13.000000", "AGGREGATION", AvgAggregation, "60", "COUNT", "120", "SELECTED_LABELS", "l1", "l2", "ALIGN", "10", "FILTER", "labels!="}},
51 | {"groupby l2 reduce max",
52 | args{0, 1, *NewMultiRangeOptions().SetGroupByReduce("l2", MaxReducer),
53 | []string{"labels!="}},
54 | []interface{}{"0", "1", "FILTER", "labels!=", "GROUPBY", "l2", "REDUCE", "MAX"}},
55 | {"groupby l2 reduce min",
56 | args{0, 1, *NewMultiRangeOptions().SetGroupByReduce("l2", MinReducer),
57 | []string{"labels!="}},
58 | []interface{}{"0", "1", "FILTER", "labels!=", "GROUPBY", "l2", "REDUCE", "MIN"}},
59 | {"groupby l2 reduce sum",
60 | args{0, 1, *NewMultiRangeOptions().SetGroupByReduce("l2", SumReducer),
61 | []string{"labels!="}},
62 | []interface{}{"0", "1", "FILTER", "labels!=", "GROUPBY", "l2", "REDUCE", "SUM"}},
63 | }
64 | for _, tt := range tests {
65 | t.Run(tt.name, func(t *testing.T) {
66 | if got := createMultiRangeCmdArguments(tt.args.fromTimestamp, tt.args.toTimestamp, tt.args.mrangeOptions, tt.args.filters); !reflect.DeepEqual(got, tt.want) {
67 | t.Errorf("CreateMultiRangeCmdArguments() = %v, want %v", got, tt.want)
68 | }
69 | })
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/pool.go:
--------------------------------------------------------------------------------
1 | package redis_timeseries_go
2 |
3 | import (
4 | "fmt"
5 | "math/rand"
6 | "sync"
7 | "time"
8 |
9 | "github.com/gomodule/redigo/redis"
10 | )
11 |
12 | type ConnPool interface {
13 | Get() redis.Conn
14 | Close() error
15 | }
16 |
17 | type SingleHostPool struct {
18 | *redis.Pool
19 | }
20 |
21 | func NewSingleHostPool(host string, authPass *string) *SingleHostPool {
22 | ret := &redis.Pool{
23 | Dial: dialFuncWrapper(host, authPass),
24 | TestOnBorrow: testOnBorrow,
25 | MaxIdle: maxConns,
26 | }
27 |
28 | return &SingleHostPool{ret}
29 | }
30 |
31 | type MultiHostPool struct {
32 | sync.Mutex
33 | pools map[string]*redis.Pool
34 | hosts []string
35 | authPass *string
36 | }
37 |
38 | func NewMultiHostPool(hosts []string, authPass *string) *MultiHostPool {
39 | return &MultiHostPool{
40 | pools: make(map[string]*redis.Pool, len(hosts)),
41 | hosts: hosts,
42 | authPass: authPass,
43 | }
44 | }
45 |
46 | func (p *MultiHostPool) Get() redis.Conn {
47 | p.Lock()
48 | defer p.Unlock()
49 |
50 | host := p.hosts[rand.Intn(len(p.hosts))]
51 | pool, found := p.pools[host]
52 |
53 | if !found {
54 | pool = &redis.Pool{
55 | Dial: dialFuncWrapper(host, p.authPass),
56 | TestOnBorrow: testOnBorrow,
57 | MaxIdle: maxConns,
58 | }
59 | p.pools[host] = pool
60 | }
61 |
62 | return pool.Get()
63 | }
64 |
65 | func dialFuncWrapper(host string, authPass *string) func() (redis.Conn, error) {
66 | return func() (redis.Conn, error) {
67 | conn, err := redis.Dial("tcp", host)
68 | if err != nil {
69 | return conn, err
70 | }
71 | if authPass != nil {
72 | _, err = conn.Do("AUTH", *authPass)
73 | }
74 | return conn, err
75 | }
76 | }
77 |
78 | func testOnBorrow(c redis.Conn, t time.Time) (err error) {
79 | if time.Since(t) > time.Millisecond {
80 | _, err = c.Do("PING")
81 | }
82 | return err
83 | }
84 |
85 | func (p *MultiHostPool) Close() (err error) {
86 | p.Lock()
87 | defer p.Unlock()
88 | for host, pool := range p.pools {
89 | poolErr := pool.Close()
90 | //preserve pool error if not nil but continue
91 | if poolErr != nil {
92 | if err == nil {
93 | err = fmt.Errorf("Error closing pool for host %s. Got %v.", host, poolErr)
94 | } else {
95 | err = fmt.Errorf("%v Error closing pool for host %s. Got %v.", err, host, poolErr)
96 | }
97 | }
98 | }
99 | return
100 | }
101 |
--------------------------------------------------------------------------------
/pool_test.go:
--------------------------------------------------------------------------------
1 | package redis_timeseries_go
2 |
3 | import (
4 | "github.com/gomodule/redigo/redis"
5 | "github.com/stretchr/testify/assert"
6 | "testing"
7 | )
8 |
9 | func TestNewMultiHostPool(t *testing.T) {
10 | type args struct {
11 | hosts []string
12 | authPass *string
13 | }
14 | tests := []struct {
15 | name string
16 | args args
17 | wantPoolSize int
18 | wantConntNil bool
19 | }{
20 | {"same connection string", args{[]string{"localhost:6379", "localhost:6379"}, nil}, 2, false},
21 | }
22 | for _, tt := range tests {
23 | t.Run(tt.name, func(t *testing.T) {
24 | got := NewMultiHostPool(tt.args.hosts, tt.args.authPass)
25 | if len(got.hosts) != tt.wantPoolSize {
26 | t.Errorf("NewMultiHostPool() = %v, want %v", got, tt.wantPoolSize)
27 | }
28 | if gotConn := got.Get(); tt.wantConntNil == false && gotConn == nil {
29 | t.Errorf("NewMultiHostPool().Get() = %v, want %v", gotConn, tt.wantConntNil)
30 | }
31 | })
32 | }
33 | }
34 |
35 | func TestMultiHostPool_Close(t *testing.T) {
36 | host, password := getTestConnectionDetails()
37 | // Test a simple flow
38 | if password == "" {
39 | oneMulti := NewMultiHostPool([]string{host}, nil)
40 | conn := oneMulti.Get()
41 | assert.NotNil(t, conn)
42 | err := oneMulti.Close()
43 | assert.Nil(t, err)
44 | err = oneMulti.Close()
45 | assert.Nil(t, err)
46 | assert.NotNil(t, conn)
47 | severalMulti := NewMultiHostPool([]string{host, host}, nil)
48 | connMulti := severalMulti.Get()
49 | assert.NotNil(t, connMulti)
50 | err = severalMulti.Close()
51 | assert.Nil(t, err)
52 | }
53 | // Exhaustive test
54 | dial := func() (redis.Conn, error) {
55 | return redis.Dial("tcp", host, redis.DialPassword(password))
56 | }
57 | pool1 := &redis.Pool{Dial: dial, MaxIdle: maxConns}
58 | pool2 := &redis.Pool{Dial: dial, MaxIdle: maxConns}
59 | pool3 := &redis.Pool{Dial: dial, MaxIdle: maxConns}
60 | //Close pull3 prior to enforce error
61 | pool3.Close()
62 | pool4 := &redis.Pool{Dial: dial, MaxIdle: maxConns}
63 |
64 | type fields struct {
65 | pools map[string]*redis.Pool
66 | hosts []string
67 | }
68 | tests := []struct {
69 | name string
70 | fields fields
71 | wantErr bool
72 | }{
73 | {"empty", fields{map[string]*redis.Pool{}, []string{}}, false},
74 | {"normal", fields{map[string]*redis.Pool{"hostpool1": pool1}, []string{"hostpool1"}}, false},
75 | {"pool3-already-close", fields{map[string]*redis.Pool{"hostpool2": pool2, "hostpool3": pool3, "hostpool4": pool4}, []string{"hostpool2", "hostpool3", "hostpool3"}}, false},
76 | }
77 | for _, tt := range tests {
78 | t.Run(tt.name, func(t *testing.T) {
79 | p := &MultiHostPool{
80 | pools: tt.fields.pools,
81 | hosts: tt.fields.hosts,
82 | }
83 | if err := p.Close(); (err != nil) != tt.wantErr {
84 | t.Errorf("Close() error = %v, wantErr %v", err, tt.wantErr)
85 | }
86 | // ensure all connections are really closed
87 | if !tt.wantErr {
88 | for _, pool := range p.pools {
89 | if _, err := pool.Get().Do("PING"); err == nil {
90 | t.Errorf("expected error after connection closed. Got %v", err)
91 | }
92 | }
93 | }
94 | })
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/range.go:
--------------------------------------------------------------------------------
1 | package redis_timeseries_go
2 |
3 | import (
4 | "fmt"
5 | "strconv"
6 | )
7 |
8 | // MultiRangeOptions represent the options for querying across multiple time-series
9 | type RangeOptions struct {
10 | AggType AggregationType
11 | TimeBucket int
12 | Count int64
13 | Align int64
14 | FilterByTs []int64
15 | FilterByValueMin *float64
16 | FilterByValueMax *float64
17 | }
18 |
19 | func NewRangeOptions() *RangeOptions {
20 | return &RangeOptions{
21 | AggType: "",
22 | TimeBucket: -1,
23 | Count: -1,
24 | Align: -1,
25 | FilterByTs: []int64{},
26 | FilterByValueMin: nil,
27 | FilterByValueMax: nil,
28 | }
29 | }
30 |
31 | // DefaultRangeOptions are the default options for querying across a time-series range
32 | var DefaultRangeOptions = *NewRangeOptions()
33 |
34 | func (rangeopts *RangeOptions) SetCount(count int64) *RangeOptions {
35 | rangeopts.Count = count
36 | return rangeopts
37 | }
38 |
39 | // SetAlign sets the time bucket alignment control for AGGREGATION.
40 | // This will control the time bucket timestamps by changing the reference timestamp on which a bucket is defined.
41 | func (rangeopts *RangeOptions) SetAlign(byTimeStamp int64) *RangeOptions {
42 | rangeopts.Align = byTimeStamp
43 | return rangeopts
44 | }
45 |
46 | // SetFilterByTs sets a list of timestamps to filter the result by specific timestamps
47 | func (rangeopts *RangeOptions) SetFilterByTs(filterByTS []int64) *RangeOptions {
48 | rangeopts.FilterByTs = filterByTS
49 | return rangeopts
50 | }
51 |
52 | // SetFilterByValue filters result by value using minimum and maximum ( inclusive )
53 | func (rangeopts *RangeOptions) SetFilterByValue(min, max float64) *RangeOptions {
54 | rangeopts.FilterByValueMin = &min
55 | rangeopts.FilterByValueMax = &max
56 | return rangeopts
57 | }
58 |
59 | func (rangeopts *RangeOptions) SetAggregation(aggType AggregationType, timeBucket int) *RangeOptions {
60 | rangeopts.AggType = aggType
61 | rangeopts.TimeBucket = timeBucket
62 | return rangeopts
63 | }
64 |
65 | func createRangeCmdArguments(key string, fromTimestamp int64, toTimestamp int64, rangeOptions RangeOptions) []interface{} {
66 | args := []interface{}{key, strconv.FormatInt(fromTimestamp, 10), strconv.FormatInt(toTimestamp, 10)}
67 | if rangeOptions.FilterByValueMin != nil {
68 | args = append(args, "FILTER_BY_VALUE",
69 | fmt.Sprintf("%f", *rangeOptions.FilterByValueMin),
70 | fmt.Sprintf("%f", *rangeOptions.FilterByValueMax))
71 | }
72 | if len(rangeOptions.FilterByTs) > 0 {
73 | args = append(args, "FILTER_BY_TS")
74 | for _, timestamp := range rangeOptions.FilterByTs {
75 | args = append(args, strconv.FormatInt(timestamp, 10))
76 | }
77 | }
78 | if rangeOptions.AggType != "" {
79 | args = append(args, "AGGREGATION", rangeOptions.AggType, strconv.Itoa(rangeOptions.TimeBucket))
80 | }
81 | if rangeOptions.Count != -1 {
82 | args = append(args, "COUNT", strconv.FormatInt(rangeOptions.Count, 10))
83 | }
84 | if rangeOptions.Align != -1 {
85 | args = append(args, "ALIGN", strconv.FormatInt(rangeOptions.Align, 10))
86 | }
87 | return args
88 | }
89 |
--------------------------------------------------------------------------------
/range_test.go:
--------------------------------------------------------------------------------
1 | package redis_timeseries_go
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 | )
7 |
8 | func TestCreateRangeCmdArguments(t *testing.T) {
9 | type args struct {
10 | key string
11 | fromTimestamp int64
12 | toTimestamp int64
13 | rangeOptions RangeOptions
14 | }
15 | tests := []struct {
16 | name string
17 | args args
18 | want []interface{}
19 | }{
20 | {"default", args{"key", 0, 1, DefaultRangeOptions}, []interface{}{"key", "0", "1"}},
21 | {"aggregation",
22 | args{"key", 0, 1, *NewRangeOptions().SetAggregation(AvgAggregation, 60)},
23 | []interface{}{"key", "0", "1", "AGGREGATION", AvgAggregation, "60"}},
24 | {"aggregation and count",
25 | args{"key", 0, 1, *NewRangeOptions().SetAggregation(AvgAggregation, 60).SetCount(120)},
26 | []interface{}{"key", "0", "1", "AGGREGATION", AvgAggregation, "60", "COUNT", "120"}},
27 | {"aggregation and align",
28 | args{"key", 0, 1, *NewRangeOptions().SetAggregation(AvgAggregation, 60).SetCount(120).SetAlign(4)},
29 | []interface{}{"key", "0", "1", "AGGREGATION", AvgAggregation, "60", "COUNT", "120", "ALIGN", "4"}},
30 | {"aggregation and filter by ts",
31 | args{"key", 0, 1, *NewRangeOptions().SetAggregation(AvgAggregation, 60).SetCount(120).SetFilterByTs([]int64{10, 5, 11})},
32 | []interface{}{"key", "0", "1", "FILTER_BY_TS", "10", "5", "11", "AGGREGATION", AvgAggregation, "60", "COUNT", "120"}},
33 | {"aggregation and filter by value",
34 | args{"key", 0, 1, *NewRangeOptions().SetAggregation(AvgAggregation, 60).SetCount(120).SetFilterByValue(5.0, 55.0)},
35 | []interface{}{"key", "0", "1", "FILTER_BY_VALUE", "5.000000", "55.000000", "AGGREGATION", AvgAggregation, "60", "COUNT", "120"}},
36 | }
37 | for _, tt := range tests {
38 | t.Run(tt.name, func(t *testing.T) {
39 | if got := createRangeCmdArguments(tt.args.key, tt.args.fromTimestamp, tt.args.toTimestamp, tt.args.rangeOptions); !reflect.DeepEqual(got, tt.want) {
40 | t.Errorf("CreateMultiRangeCmdArguments() = %v, want %v", got, tt.want)
41 | }
42 | })
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/reply_parser.go:
--------------------------------------------------------------------------------
1 | package redis_timeseries_go
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "github.com/gomodule/redigo/redis"
7 | "strconv"
8 | )
9 |
10 | func toAggregationType(aggType interface{}) (aggTypeStr AggregationType, err error) {
11 | agg, err := redis.String(aggType, nil)
12 | if err != nil {
13 | return
14 | }
15 | aggTypeStr = (AggregationType)(agg)
16 | return
17 | }
18 |
19 | func toDuplicatePolicy(duplicatePolicy interface{}) (duplicatePolicyStr DuplicatePolicyType, err error) {
20 | duplicatePolicyStr = ""
21 | if duplicatePolicy == nil {
22 | return
23 | }
24 | policy, err := redis.String(duplicatePolicy, nil)
25 | if err != nil {
26 | return
27 | }
28 | duplicatePolicyStr = (DuplicatePolicyType)(policy)
29 | return
30 | }
31 |
32 | func ParseRules(ruleInterface interface{}, err error) (rules []Rule, retErr error) {
33 | if err != nil {
34 | return nil, err
35 | }
36 | ruleSlice, err := redis.Values(ruleInterface, nil)
37 | if err != nil {
38 | return nil, err
39 | }
40 | for _, ruleSlice := range ruleSlice {
41 |
42 | ruleValues, err := redis.Values(ruleSlice, nil)
43 | if err != nil {
44 | return nil, err
45 | }
46 | destKey, err := redis.String(ruleValues[0], nil)
47 | if err != nil {
48 | return nil, err
49 | }
50 | bucketSizeSec, err := redis.Int(ruleValues[1], nil)
51 | if err != nil {
52 | return nil, err
53 | }
54 | aggType, err := toAggregationType(ruleValues[2])
55 | if err != nil {
56 | return nil, err
57 | }
58 | rules = append(rules, Rule{destKey, bucketSizeSec, aggType})
59 | }
60 | return rules, nil
61 | }
62 |
63 | func ParseInfo(result interface{}, err error) (info KeyInfo, outErr error) {
64 | values, outErr := redis.Values(result, err)
65 | if outErr != nil {
66 | return KeyInfo{}, err
67 | }
68 | if len(values)%2 != 0 {
69 | return KeyInfo{}, errors.New("ParseInfo expects even number of values result")
70 | }
71 | var key string
72 | for i := 0; i < len(values); i += 2 {
73 | key, outErr = redis.String(values[i], nil)
74 | switch key {
75 | case "rules":
76 | info.Rules, outErr = ParseRules(values[i+1], nil)
77 | case "retentionTime":
78 | info.RetentionTime, outErr = redis.Int64(values[i+1], nil)
79 | case "chunkCount":
80 | info.ChunkCount, outErr = redis.Int64(values[i+1], nil)
81 | // Backwards compatible
82 | case "maxSamplesPerChunk":
83 | var v int64
84 | v, outErr = redis.Int64(values[i+1], nil)
85 | info.MaxSamplesPerChunk = v
86 | info.ChunkSize = 16 * v
87 | case "chunkSize":
88 | info.ChunkSize, outErr = redis.Int64(values[i+1], nil)
89 | case "lastTimestamp":
90 | info.LastTimestamp, outErr = redis.Int64(values[i+1], nil)
91 | case "labels":
92 | info.Labels, outErr = ParseLabels(values[i+1])
93 | case "duplicatePolicy":
94 | info.DuplicatePolicy, outErr = toDuplicatePolicy(values[i+1])
95 | }
96 | if outErr != nil {
97 | return KeyInfo{}, outErr
98 | }
99 | }
100 |
101 | return info, nil
102 | }
103 |
104 | func ParseDataPoints(info interface{}) (dataPoints []DataPoint, err error) {
105 | dataPoints = make([]DataPoint, 0)
106 | values, err := redis.Values(info, err)
107 | if err != nil {
108 | return
109 | }
110 | for _, rawDataPoint := range values {
111 | var dataPoint *DataPoint = nil //nolint:ineffassign
112 | dataPoint, err = ParseDataPoint(rawDataPoint)
113 | if err != nil {
114 | return
115 | }
116 | if dataPoint != nil {
117 | dataPoints = append(dataPoints, *dataPoint)
118 | }
119 | }
120 | return
121 | }
122 |
123 | func ParseDataPoint(rawDataPoint interface{}) (dataPoint *DataPoint, err error) {
124 | dataPoint = nil
125 | iValues, err := redis.Values(rawDataPoint, nil)
126 | if err != nil || len(iValues) == 0 {
127 | return
128 | }
129 | if len(iValues) != 2 {
130 | err = fmt.Errorf("ParseDataPoint expects array reply of size 2 with timestamp and value.Got %v", iValues)
131 | return
132 | }
133 | timestamp, err := redis.Int64(iValues[0], nil)
134 | if err != nil {
135 | return
136 | }
137 | value, err := redis.String(iValues[1], nil)
138 | if err != nil {
139 | return
140 | }
141 | float, err := strconv.ParseFloat(value, 64)
142 | if err != nil {
143 | return
144 | }
145 | dataPoint = NewDataPoint(timestamp, float)
146 | return
147 | }
148 |
149 | func ParseLabels(res interface{}) (labels map[string]string, err error) {
150 | values, err := redis.Values(res, err)
151 | if err != nil {
152 | return
153 | }
154 | labels = make(map[string]string, len(values))
155 | for i := 0; i < len(values); i++ {
156 | iValues, err := redis.Values(values[i], err)
157 | if err != nil {
158 | return nil, err
159 | }
160 | if len(iValues) != 2 {
161 | err = errors.New("ParseLabels: expects 2 elements per inner-array")
162 | return nil, err
163 | }
164 | key, okKey := iValues[0].([]byte)
165 | value, okValue := iValues[1].([]byte)
166 | if !okKey || !okValue {
167 | err = errors.New("ParseLabels: StringMap key not a bulk string value")
168 | return nil, err
169 | }
170 | labels[string(key)] = string(value)
171 | }
172 | return
173 | }
174 |
175 | func ParseRanges(info interface{}) (ranges []Range, err error) {
176 | values, err := redis.Values(info, err)
177 | if err != nil {
178 | return nil, err
179 | }
180 | if len(values) == 0 {
181 | return []Range{}, nil
182 | }
183 | for _, i := range values {
184 | iValues, err := redis.Values(i, err)
185 | if err != nil {
186 | return nil, err
187 | }
188 | if len(iValues) != 3 {
189 | err = errors.New("ParseRanges: expects 3 elements per inner-array")
190 | return nil, err
191 | }
192 |
193 | name, err := redis.String(iValues[0], nil)
194 | if err != nil {
195 | return nil, err
196 | }
197 |
198 | labels, err := ParseLabels(iValues[1])
199 | if err != nil {
200 | return nil, err
201 | }
202 |
203 | dataPoints, err := ParseDataPoints(iValues[2])
204 | if err != nil {
205 | return nil, err
206 | }
207 | r := Range{name, labels, dataPoints}
208 | ranges = append(ranges, r)
209 | }
210 | return
211 | }
212 |
213 | func ParseRangesSingleDataPoint(info interface{}) (ranges []Range, err error) {
214 | values, err := redis.Values(info, err)
215 |
216 | if err != nil {
217 | return nil, err
218 | }
219 | if len(values) == 0 {
220 | return []Range{}, nil
221 | }
222 | for _, i := range values {
223 | dataPoints := make([]DataPoint, 0)
224 |
225 | iValues, err := redis.Values(i, err)
226 | if err != nil {
227 | return nil, err
228 | }
229 | if len(iValues) != 3 {
230 | err = errors.New("ParseRanges: expects 3 elements per inner-array")
231 | return nil, err
232 | }
233 |
234 | name, err := redis.String(iValues[0], nil)
235 | if err != nil {
236 | return nil, err
237 | }
238 |
239 | labels, err := ParseLabels(iValues[1])
240 | if err != nil {
241 | return nil, err
242 | }
243 | var dataPoint *DataPoint = nil //nolint:ineffassign
244 | dataPoint, err = ParseDataPoint(iValues[2])
245 | if err != nil {
246 | return nil, err
247 | }
248 | if dataPoint != nil {
249 | dataPoints = append(dataPoints, *dataPoint)
250 | }
251 | if err != nil {
252 | return nil, err
253 | }
254 | r := Range{name, labels, dataPoints}
255 | ranges = append(ranges, r)
256 | }
257 | return
258 | }
259 |
--------------------------------------------------------------------------------
/reply_parser_test.go:
--------------------------------------------------------------------------------
1 | package redis_timeseries_go
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 | )
7 |
8 | func TestParseLabels(t *testing.T) {
9 | type args struct {
10 | res interface{}
11 | }
12 | tests := []struct {
13 | name string
14 | args args
15 | wantLabels map[string]string
16 | wantErr bool
17 | }{
18 | {"correctInput",
19 | args{[]interface{}{[]interface{}{[]byte("hostname"), []byte("host_3")}, []interface{}{[]byte("region"), []byte("us-west-2")}}},
20 | map[string]string{"hostname": "host_3", "region": "us-west-2"},
21 | false,
22 | },
23 | {"IncorrectInput",
24 | args{[]interface{}{[]interface{}{[]byte("hostname"), []byte("host_3")}, []interface{}{[]byte("region")}}},
25 | nil,
26 | true,
27 | },
28 | }
29 | for _, tt := range tests {
30 | t.Run(tt.name, func(t *testing.T) {
31 | gotLabels, err := ParseLabels(tt.args.res)
32 | if (err != nil) != tt.wantErr {
33 | t.Errorf("ParseLabels() error = %v, wantErr %v", err, tt.wantErr)
34 | return
35 | }
36 | if !reflect.DeepEqual(gotLabels, tt.wantLabels) {
37 | t.Errorf("ParseLabels() gotLabels = %v, want %v", gotLabels, tt.wantLabels)
38 | }
39 | })
40 | }
41 | }
42 |
43 | func TestParseRangesSingleDataPoint(t *testing.T) {
44 | type args struct {
45 | info interface{}
46 | }
47 | tests := []struct {
48 | name string
49 | args args
50 | wantRanges []Range
51 | wantErr bool
52 | }{
53 | {"empty input",
54 | args{[]interface{}{}},
55 | []Range{},
56 | false,
57 | },
58 | {"correct input",
59 | args{[]interface{}{[]interface{}{[]byte("serie 1"), []interface{}{}, []interface{}{[]byte("1"), []byte("1")}}}},
60 | []Range{Range{"serie 1", map[string]string{}, []DataPoint{{1, 1.0}}}},
61 | false,
62 | },
63 | {"incorrect input ( 2 elements on inner array )",
64 | args{[]interface{}{[]interface{}{[]byte("serie 1"), []interface{}{}}}},
65 | []Range{},
66 | true,
67 | },
68 | }
69 | for _, tt := range tests {
70 | t.Run(tt.name, func(t *testing.T) {
71 | gotRanges, err := ParseRangesSingleDataPoint(tt.args.info)
72 | if (err != nil) != tt.wantErr {
73 | t.Errorf("ParseRangesSingleDataPoint() error = %v, wantErr %v", err, tt.wantErr)
74 | return
75 | }
76 | if !reflect.DeepEqual(gotRanges, tt.wantRanges) && tt.wantErr == false {
77 | t.Errorf("ParseRangesSingleDataPoint() gotRanges = %v, want %v", gotRanges, tt.wantRanges)
78 | }
79 | })
80 | }
81 | }
82 |
83 | func TestParseDataPoint(t *testing.T) {
84 | type args struct {
85 | rawDataPoint interface{}
86 | }
87 | tests := []struct {
88 | name string
89 | args args
90 | wantDataPoint *DataPoint
91 | wantErr bool
92 | }{
93 | {"empty input",
94 | args{[]interface{}{}},
95 | nil,
96 | false,
97 | },
98 | {"correct input",
99 | args{[]interface{}{[]byte("1"), []byte("1")}},
100 | &DataPoint{1, 1.0},
101 | false,
102 | },
103 | {"incorrect input size",
104 | args{[]interface{}{[]byte("1"), []byte("1"), []byte("1")}},
105 | &DataPoint{1, 1.0},
106 | true,
107 | },
108 | {"incorrect input value of timestamp",
109 | args{[]interface{}{[]byte("a a"), []byte("1"), []byte("1")}},
110 | nil,
111 | true,
112 | },
113 | {"incorrect input value of value",
114 | args{[]interface{}{[]byte("1"), []byte("A A"), []byte("1")}},
115 | nil,
116 | true,
117 | },
118 | }
119 | for _, tt := range tests {
120 | t.Run(tt.name, func(t *testing.T) {
121 | gotDataPoint, err := ParseDataPoint(tt.args.rawDataPoint)
122 | if (err != nil) != tt.wantErr {
123 | t.Errorf("ParseDataPoint() error = %v, wantErr %v", err, tt.wantErr)
124 | return
125 | }
126 | if !reflect.DeepEqual(gotDataPoint, tt.wantDataPoint) && tt.wantErr == false {
127 | t.Errorf("ParseDataPoint() gotDataPoint = %v, want %v", gotDataPoint, tt.wantDataPoint)
128 | }
129 | })
130 | }
131 | }
132 |
133 | func TestParseDataPoints(t *testing.T) {
134 | type args struct {
135 | info interface{}
136 | }
137 | tests := []struct {
138 | name string
139 | args args
140 | wantDataPoints []DataPoint
141 | wantErr bool
142 | }{
143 | {"empty input",
144 | args{[]interface{}{}},
145 | []DataPoint{},
146 | false,
147 | },
148 | {"correct input one datapoints",
149 | args{[]interface{}{[]interface{}{[]byte("1"), []byte("1")}}},
150 | []DataPoint{{1, 1.0}},
151 | false,
152 | },
153 | {"correct input two datapoints",
154 | args{[]interface{}{[]interface{}{[]byte("1"), []byte("1")}, []interface{}{[]byte("2"), []byte("2")}}},
155 | []DataPoint{{1, 1.0}, {2, 2.0}},
156 | false,
157 | },
158 | {"incorrect input Nan on timestamp",
159 | args{[]interface{}{[]interface{}{[]byte("A"), []byte("1")}, []interface{}{[]byte("2"), []byte("2")}}},
160 | []DataPoint{},
161 | true,
162 | },
163 | {"incorrect input Nan on value",
164 | args{[]interface{}{[]interface{}{[]byte("1"), []byte("A")}, []interface{}{[]byte("2"), []byte("2")}}},
165 | []DataPoint{},
166 | true,
167 | },
168 | }
169 | for _, tt := range tests {
170 | t.Run(tt.name, func(t *testing.T) {
171 | gotDataPoints, err := ParseDataPoints(tt.args.info)
172 | if (err != nil) != tt.wantErr {
173 | t.Errorf("ParseDataPoints() error = %v, wantErr %v", err, tt.wantErr)
174 | return
175 | }
176 | if !reflect.DeepEqual(gotDataPoints, tt.wantDataPoints) {
177 | t.Errorf("ParseDataPoints() gotDataPoints = %v, want %v", gotDataPoints, tt.wantDataPoints)
178 | }
179 | })
180 | }
181 | }
182 |
183 | func TestParseRanges(t *testing.T) {
184 | type args struct {
185 | info interface{}
186 | }
187 | tests := []struct {
188 | name string
189 | args args
190 | wantRanges []Range
191 | wantErr bool
192 | }{
193 | {"empty input",
194 | args{[]interface{}{}},
195 | []Range{},
196 | false,
197 | },
198 | {"correct input",
199 | args{[]interface{}{[]interface{}{[]byte("serie 1"), []interface{}{}, []interface{}{[]interface{}{[]byte("1"), []byte("1")}}}}},
200 | []Range{Range{"serie 1", map[string]string{}, []DataPoint{{1, 1.0}}}},
201 | false,
202 | },
203 | {"incorrect input ( 2 elements on inner array )",
204 | args{[]interface{}{[]interface{}{[]byte("serie 1"), []interface{}{}}}},
205 | []Range{},
206 | true,
207 | },
208 | {"incorrect input ( bad datapoint timestamp )",
209 | args{[]interface{}{[]interface{}{[]byte("serie 1"), []interface{}{}, []interface{}{[]interface{}{[]byte("AA"), []byte("1")}}}}},
210 | []Range{Range{"serie 1", map[string]string{}, []DataPoint{}}},
211 | true,
212 | },
213 | {"incorrect input ( bad datapoint value )",
214 | args{[]interface{}{[]interface{}{[]byte("serie 1"), []interface{}{}, []interface{}{[]interface{}{[]byte("1"), []byte("AA")}}}}},
215 | []Range{Range{"serie 1", map[string]string{}, []DataPoint{}}},
216 | true,
217 | },
218 | }
219 | for _, tt := range tests {
220 | t.Run(tt.name, func(t *testing.T) {
221 | gotRanges, err := ParseRanges(tt.args.info)
222 | if (err != nil) != tt.wantErr {
223 | t.Errorf("ParseRanges() error = %v, wantErr %v", err, tt.wantErr)
224 | return
225 | }
226 | if !reflect.DeepEqual(gotRanges, tt.wantRanges) && tt.wantErr == false {
227 | t.Errorf("ParseRanges() gotRanges = %v, want %v", gotRanges, tt.wantRanges)
228 | }
229 | })
230 | }
231 | }
232 |
--------------------------------------------------------------------------------
/run_tests.sh:
--------------------------------------------------------------------------------
1 | docker build -t redis_timeseries_tests . && docker run --rm -it redis_timeseries_tests
2 |
--------------------------------------------------------------------------------