├── .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 | [![license](https://img.shields.io/github/license/RedisTimeSeries/RedisTimeSeries-go.svg)](https://github.com/RedisTimeSeries/RedisTimeSeries-go) 2 | [![CircleCI](https://circleci.com/gh/RedisTimeSeries/redistimeseries-go.svg?style=svg)](https://circleci.com/gh/RedisTimeSeries/redistimeseries-go) 3 | [![GitHub issues](https://img.shields.io/github/release/RedisTimeSeries/redistimeseries-go.svg)](https://github.com/RedisTimeSeries/redistimeseries-go/releases/latest) 4 | [![Codecov](https://codecov.io/gh/RedisTimeSeries/redistimeseries-go/branch/master/graph/badge.svg)](https://codecov.io/gh/RedisTimeSeries/redistimeseries-go) 5 | [![GoDoc](https://godoc.org/github.com/RedisTimeSeries/redistimeseries-go?status.svg)](https://godoc.org/github.com/RedisTimeSeries/redistimeseries-go) 6 | [![Go Report Card](https://goreportcard.com/badge/github.com/RedisTimeSeries/redistimeseries-go)](https://goreportcard.com/report/github.com/RedisTimeSeries/redistimeseries-go) 7 | [![Total alerts](https://img.shields.io/lgtm/alerts/g/RedisTimeSeries/redistimeseries-go.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/RedisTimeSeries/redistimeseries-go/alerts/) 8 | 9 | # redistimeseries-go 10 | [![Forum](https://img.shields.io/badge/Forum-RedisTimeSeries-blue)](https://forum.redislabs.com/c/modules/redistimeseries) 11 | [![Discord](https://img.shields.io/discord/697882427875393627?style=flat-square)](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) | | 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) | | 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 | --------------------------------------------------------------------------------