├── .circleci └── config.yml ├── .dockerignore ├── .gitignore ├── .golangci.yml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── ROADMAP.md ├── cmd └── cmd.go ├── go.mod ├── go.sum ├── go.tools.go ├── logger └── logger.go ├── parser ├── count-distinct.go ├── count.go ├── distinct.go ├── parser.go ├── parser_easyjson.go └── search.go ├── server └── server.go ├── sqlquery ├── cases.go ├── dates.go └── query.go └── tidalwave.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/golang:1.14 6 | 7 | working_directory: ~/tidalwave 8 | 9 | steps: 10 | - checkout 11 | - restore_cache: 12 | keys: 13 | - gosum-{{ checksum "go.sum" }} 14 | - run: curl -L "https://github.com/dustinblackman/gomodrun/releases/download/v0.2.2/gomodrun_0.2.2_linux_amd64.tar.gz" | tar -zxvf - -C /go/bin/ gomodrun 15 | - run: go mod download 16 | - run: make lint 17 | - save_cache: 18 | key: gosum 19 | paths: 20 | - .gomodrun 21 | - /go/pkg/mod 22 | 23 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | vendor/ 3 | logs/ 4 | dist/ 5 | tmp/ 6 | 7 | tidalwave 8 | !vendor/vendor.json 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | tmp/ 3 | logs/ 4 | .gobincache/ 5 | .gomodrun/ 6 | vendor/*/ 7 | !vendor/vendor.json 8 | 9 | tidalwave 10 | tidalwave.json 11 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters-settings: 2 | depguard: 3 | list-type: blacklist 4 | dupl: 5 | threshold: 100 6 | funlen: 7 | lines: 300 8 | statements: 50 9 | goconst: 10 | min-len: 4 11 | min-occurrences: 5 12 | gocritic: 13 | enabled-tags: 14 | - diagnostic 15 | - experimental 16 | - opinionated 17 | - performance 18 | - style 19 | disabled-checks: 20 | - dupImport # https://github.com/go-critic/go-critic/issues/845 21 | - ifElseChain 22 | settings: # settings passed to gocritic 23 | rangeValCopy: 24 | sizeThreshold: 1024 25 | hugeParam: 26 | sizeThreshold: 1024 27 | gocyclo: 28 | min-complexity: 40 29 | goimports: 30 | local-prefixes: github.com/dustinblackman/gomodrun 31 | golint: 32 | min-confidence: 0 33 | govet: 34 | check-shadowing: true 35 | lll: 36 | line-length: 300 37 | maligned: 38 | suggest-new: true 39 | misspell: 40 | locale: US 41 | 42 | issues: 43 | exclude-use-default: false 44 | exclude-rules: 45 | - path: _test\.go 46 | linters: 47 | - dupl 48 | - text: "(G204|G304|G302):" 49 | linters: 50 | - gosec 51 | 52 | linters: 53 | disable-all: true 54 | enable: 55 | - bodyclose 56 | - deadcode 57 | - depguard 58 | - dogsled 59 | - dupl 60 | - errcheck 61 | - funlen 62 | - gochecknoinits 63 | - goconst 64 | - gocritic 65 | - gocyclo 66 | - gofmt 67 | - goimports 68 | - golint 69 | - goprintffuncname 70 | - gosec 71 | - gosimple 72 | - govet 73 | - ineffassign 74 | - interfacer 75 | - lll 76 | - maligned 77 | - misspell 78 | - nakedret 79 | - rowserrcheck 80 | - scopelint 81 | - staticcheck 82 | - structcheck 83 | - stylecheck 84 | - typecheck 85 | - unconvert 86 | - unparam 87 | - unused 88 | - varcheck 89 | - whitespace 90 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Builder 2 | FROM golang:1.12-alpine3.9 as builder 3 | 4 | RUN apk add --no-cache git build-base bash 5 | 6 | WORKDIR /build 7 | COPY ./go.mod ./go.sum ./ 8 | RUN go mod download 9 | 10 | COPY . . 11 | RUN make 12 | 13 | # App 14 | FROM alpine:3.9 15 | 16 | RUN apk add --no-cache bash ca-certificates && rm -rf /usr/share/man /tmp/* /var/tmp/* 17 | COPY --from=builder /build/tidalwave /usr/bin/tidalwave 18 | 19 | CMD ["/usr/bin/tidalwave"] 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2019 Dustin Blackman 4 | Copyright (c) 2019 Busbud 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION := 1.3.3 2 | 3 | # Creates binary 4 | build: 5 | go build -x -ldflags="-X github.com/busbud/tidalwave/cmd.version=$(VERSION)" -o tidalwave tidalwave.go 6 | 7 | # Creates easyjson file for parser/parser.go 8 | easyjson: 9 | gomodrun easyjson parser/parser.go 10 | 11 | # Builds and installs binary. Mainly used from people wanting to install from source. 12 | install: 13 | go install -ldflags="-X github.com/busbud/tidalwave/cmd.version $(VERSION)" 14 | 15 | # Runs tests 16 | lint: 17 | gomodrun golangci-lint run 18 | 19 | lint-fix: 20 | gomodrun golangci-lint run --fix 21 | 22 | test: 23 | make lint 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tidalwave 2 | 3 | _JSON log file parsing with Postgres SQL_ 4 | 5 | Build Status 6 | 7 | __New Home!__ Tidalwave has moved to Busbud, where it should get a lot more love than it was getting before on [github.com/dustinblackman](https://github.com/dustinblackman). 8 | 9 | Tidalwave is an awesomely fast command line, and server for parsing JSON logs. It's meant to be an alternative to application suites like ELK which can be rather resource hungry, where Tidalwave only consumes resources when a search is in progress. It's recorded at being 8 times faster than grep with more in depth parsing than simple regex matching. 10 | 11 | Tidalwave works best with logging modules such as [logrus](https://github.com/Sirupsen/logrus), [bunyan](https://github.com/trentm/node-bunyan), [slf4j](https://github.com/savoirtech/slf4j-json-logger), [python-json-logger](https://github.com/madzak/python-json-logger), [json_logger](https://github.com/rsolomo/json_logger) or anything else that outputs JSON logs. It uses Postgres' SQL parser for handling queryies and using them logs. 12 | 13 | This project is in it's early stages where it's littered with TODOs, possible bugs, outdated docs, and all the other nifty things that come with early development. 14 | 15 | ## [Features / Roadmap](./ROADMAP.md) 16 | 17 | ## How it Works 18 | 19 | Products like ELK work by having multiple layers process' to manage and query logs. Elastic search can get quite hungry, and 3rd party services that do something similar is just too expensive for small applications. Tidalwave works by having a folder and file structure that acts as an index, then matching those files to the given query. It only takes up resources on search by taking advantage of multi core systems to quickly parse large log files. Tidalwave is meant to be CPU intensive on queries, but remains on very low resources when idle. 20 | 21 | The SQL parser can do basic math (`==`, `!=`, `<=`, `>`, ect) that works with strings, numbers, and date. Parsing multiple applications is as simple as (`SELECT * FROM serverapp, clientapp`). It can also truncate logs to reduce response size (`SELECT time, line.cmd FROM serverapp`). 22 | 23 | `date` is a special work as you'll find in more time series applications that's used for. You can either pass a date (`SELECT * FROM serverapp WHERE date = '2016-01-01'`), or pass a full timestamp (`SELECT * FROM serverapp WHERE date = '2016-01-01T01:30:00'`). 24 | 25 | ### Example 26 | 27 | Folder structure is sorted by application name, folder with date, then file names with datetime split by hour. 28 | 29 | __Folder Structure__ 30 | ``` 31 | . 32 | +-- serverapp 33 | | +-- 2016-10-01 34 | | | +-- 2016-10-01T01_00_00.log 35 | | | +-- 2016-10-01T02_00_00.log 36 | | +-- 2016-10-02 37 | | | +-- 2016-10-02T01_00_00.log 38 | | | +-- 2016-10-02T02_00_00.log 39 | | | +-- 2016-10-02T03_00_00.log 40 | | | +-- 2016-10-02T04_00_00.log 41 | | | +-- 2016-10-02T05_00_00.log 42 | | +-- 2016-10-03 43 | +-- clientapp 44 | ``` 45 | 46 | `2016-10-02T01_00_00.log` was created by the Docker client logger, where the application was using Bunyan to output it's logs. 47 | 48 | ``` 49 | ... 50 | {"v":3,"id":"49aa6ad41125","image":"docker-image","name":"server","line":{"name":"server","hostname":"49aa6ad41125","pid":14,"level":30,"cmd":"lol","suffix":"status","msg":"cmd","time":"2016-10-02T00:04:25.172Z","v":0},"host":"a2197bfa39c7"} 51 | {"v":0,"id":"49aa6ad41125","image":"docker-image","name":"server","line":{"name":"server","hostname":"49aa6ad41125","pid":14,"level":30,"cmd":"chat","suffix":"What time is it?","msg":"cmd","time":"2016-10-02T00:04:25.629Z","v":0},"host":"a2197bfa39c7"} 52 | {"v":0,"id":"49aa6ad41125","image":"docker-image","name":"server","line":{"name":"server","hostname":"49aa6ad41125","pid":14,"level":30,"cmd":"chat","suffix":"Pizza.","msg":"cmd","time":"2016-10-02T00:04:33.164Z","v":0},"host":"a2197bfa39c7"} 53 | {"v":0,"id":"49aa6ad41125","image":"docker-image","name":"server","line":{"name":"server","hostname":"49aa6ad41125","pid":14,"level":30,"cmd":"meme","suffix":"fry1 \"meme\"","msg":"cmd","time":"2016-10-02T00:04:35.811Z","v":0},"host":"a2197bfa39c7"} 54 | {"v":0,"id":"49aa6ad41125","image":"docker-image","name":"server","line":{"name":"server","hostname":"49aa6ad41125","pid":14,"level":30,"cmd":"lol","suffix":"status","msg":"cmd","time":"2016-10-02T00:04:36.066Z","v":0},"host":"a2197bfa39c7"} 55 | ... 56 | ``` 57 | 58 | Querying all the lines where `cmd` equals `chat` within a set timeframe is as simple as querying a SQL database! 59 | 60 | __Query:__ 61 | ``` 62 | SELECT * FROM serverapp WHERE line.cmd = 'chat' and date <= '2016-10-02' and date > '2016-10-02T02:00:00' 63 | ``` 64 | 65 | __Result:__ 66 | 67 | ``` 68 | {"v":0,"id":"49aa6ad41125","image":"docker-image","name":"server","line":{"name":"server","hostname":"49aa6ad41125","pid":14,"level":30,"cmd":"chat","suffix":"What time is it?","msg":"cmd","time":"2016-10-02T00:04:25.629Z","v":0},"host":"a2197bfa39c7"} 69 | {"v":0,"id":"49aa6ad41125","image":"docker-image","name":"server","line":{"name":"server","hostname":"49aa6ad41125","pid":14,"level":30,"cmd":"chat","suffix":"Pizza.","msg":"cmd","time":"2016-10-02T00:04:33.164Z","v":0},"host":"a2197bfa39c7"} 70 | ``` 71 | 72 | ## Install 73 | 74 | Grab the latest release from the [releases](https://github.com/busbud/tidalwave/releases) page, or build from source and install directly from master. Tidalwave is currently built and tested against Go 1.11. A [docker image](https://hub.docker.com/r/busbud/tidalwave/) is also available. 75 | 76 | __Quick install for Linux:__ 77 | ``` 78 | curl -Ls "https://github.com/busbud/tidalwave/releases/download/1.0.0/tidalwave-linux-amd64-1.0.0.tar.gz" | tar xz -C /usr/local/bin/ 79 | ``` 80 | 81 | __Build From Source:__ 82 | 83 | A makefile exists to handle all things needed to build and install from source. 84 | 85 | ``` 86 | git pull https://github.com/busbud/tidalwave 87 | cd tidalwave 88 | make install 89 | ``` 90 | 91 | 92 | ## Usage/Configuration 93 | 94 | Configuration can be done either by command line parameters, environment variables, or a JSON file. Please see all available flags with `tidalwave --help`. 95 | 96 | To set a configuration, you can take the flag name and export it in your environment or save in one of the three locations for config files. 97 | 98 | ### Examples 99 | 100 | __Flag:__ 101 | ``` 102 | tidalwave --client --max-parallelism 2 103 | ``` 104 | 105 | __Environment:__ 106 | ``` 107 | export TIDALWAVE_CLIENT=true 108 | export TIDALWAVE_MAX_PARALLELISM=2 109 | ``` 110 | 111 | __JSON File:__ 112 | 113 | Configuration files can be stored in one of the three locations 114 | 115 | ```sh 116 | ./tidalwave.json 117 | /etc/tidalwave.json 118 | $HOME/.tidalwave/tidalwave.json 119 | ``` 120 | ```json 121 | { 122 | "client": true, 123 | "max-parallelism": 2 124 | } 125 | ``` 126 | -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 1 | ## Features / Roadmap 2 | 3 | #### Server 4 | - [x] API Server 5 | - [x] Websockets support for live tail 6 | - [ ] Remote logging endpoints 7 | 8 | #### Command Line 9 | - [x] Querying by command line 10 | - [ ] Parse specific file rather then parsing folder index 11 | 12 | #### Clients 13 | - [x] File watch client 14 | - [x] Docker container client 15 | - [x] PID file watch client 16 | - [ ] Syslog parsing 17 | - [ ] Submitting logs to remote Tidalwave server 18 | 19 | #### Queries 20 | - [x] `SELECT *`, `SELECT line.cmd`, `SELECT * FROM foo, bar` 21 | - [x] `SELECT COUNT(*)` 22 | - [x] `SELECT DISTINCT(*)` 23 | - [x] `SELECT COUNT(DISTINCT(*))` 24 | - [x] `SELECT * FROM app WHERE key IN ('a', 'b')` 25 | - [x] `SELECT * FROM app WHERE key LIKE '%val%'` 26 | - [x] `SELECT * FROM app WHERE key ILIKE '%vAl%'` 27 | - [x] `SELECT * FROM app WHERE date BETWEEN '2017-04-10' AND '2017-04-12';` 28 | - [ ] `SELECT * FROM app LIMIT 1` 29 | - [ ] `GROUP BY` 30 | 31 | #### Dev 32 | - [x] Verbose parameter 33 | - [x] Linter 34 | - [x] Bash autocomplete 35 | - [ ] Tests 36 | - [ ] Benchmarks 37 | -------------------------------------------------------------------------------- /cmd/cmd.go: -------------------------------------------------------------------------------- 1 | // Package cmd handles initializing Tidalwave on the command line. 2 | package cmd 3 | 4 | import ( 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "os" 9 | "runtime" 10 | "strings" 11 | 12 | "github.com/busbud/tidalwave/logger" 13 | "github.com/busbud/tidalwave/parser" 14 | "github.com/busbud/tidalwave/server" 15 | "github.com/spf13/cobra" 16 | "github.com/spf13/pflag" 17 | "github.com/spf13/viper" 18 | ) 19 | 20 | var ( 21 | version = "HEAD" 22 | ) 23 | 24 | func maxParallelism() int { 25 | maxProcs := runtime.GOMAXPROCS(0) 26 | numCPU := runtime.NumCPU() 27 | if maxProcs < numCPU { 28 | return maxProcs 29 | } 30 | return numCPU 31 | } 32 | 33 | func cliQuery() { 34 | query := viper.GetString("query") 35 | if query == "-" { 36 | queryBytes, err := ioutil.ReadAll(os.Stdin) 37 | if err != nil { 38 | panic(err) 39 | } 40 | query = strings.TrimSpace(string(queryBytes)) 41 | } 42 | 43 | results := parser.Query(query) 44 | 45 | switch res := results.(type) { 46 | case parser.ChannelResults: 47 | for line := range res.Channel { 48 | _, err := os.Stdout.Write(line) 49 | if err != nil { 50 | logger.Log.Debug(err) 51 | } 52 | _, err = os.Stdout.Write([]byte("\n")) 53 | if err != nil { 54 | logger.Log.Debug(err) 55 | } 56 | } 57 | case parser.ArrayResults: 58 | for _, line := range *res.Results { 59 | fmt.Println(line) 60 | } 61 | case parser.ObjectResults: 62 | str, err := json.Marshal(res.Results) 63 | if err != nil { 64 | logger.Log.Error("Error converting object results to JSON", err) 65 | return 66 | } 67 | fmt.Println(string(str)) 68 | case parser.IntResults: 69 | fmt.Println(res.Results) 70 | } 71 | } 72 | 73 | func run(rootCmd *cobra.Command, args []string) { 74 | viper.AutomaticEnv() 75 | err := viper.ReadInConfig() 76 | 77 | // Init's global logger 78 | logger.Init(viper.GetBool("debug")) 79 | 80 | if err != nil { 81 | logger.Log.Debug(err) 82 | } 83 | 84 | // Server and Client 85 | if viper.GetBool("server") { 86 | server.New(version) 87 | } 88 | 89 | // Cli 90 | if viper.GetString("query") != "" { 91 | cliQuery() 92 | } 93 | 94 | // If here and no query is set, then no proper flags were passed. 95 | if viper.GetString("query") == "" { 96 | err = rootCmd.Help() 97 | if err != nil { 98 | logger.Log.Fatal(err) 99 | } 100 | } 101 | } 102 | 103 | // New creaes a new combra command instance. 104 | // This really only exists to make bash auto completion easier to generate. 105 | func New() *cobra.Command { 106 | rootCmd := &cobra.Command{ 107 | Use: "tidalwave", 108 | Example: ` tidalwave -q "SELECT * FROM myapp WHERE line.cmd = 'uptime' AND date > '2016-10-10'"`, 109 | Run: run, 110 | Short: "A awesomely fast JSON log parsing application queryable with SQL", 111 | Long: `Tidalwave is an awesomely fast command line, and server for parsing JSON logs. 112 | 113 | Version: ` + version + ` 114 | Home: https://github.com/busbud/tidalwave`, 115 | } 116 | 117 | flags := rootCmd.PersistentFlags() 118 | // Shared Flags 119 | flags.Int("max-parallelism", maxParallelism(), 120 | "Set the maximum amount of threads to run when processing log files during queries. Default is the number of cores on system.") 121 | flags.StringP("logroot", "r", "./logs", "Log root directory where log files are stored") 122 | flags.Bool("debug", false, "Enable debug logging") 123 | 124 | // Cli Flags 125 | flags.StringP("query", "q", "", "SQL query to execute against logs. '-' is accepted for piping in from stdin.") 126 | flags.Bool("skip-sort", false, "Skips sorting search queries, outputting lines as soon as they're found") 127 | 128 | // Server 129 | flags.BoolP("server", "s", false, "Start in server mode") 130 | flags.String("host", "0.0.0.0", "Set host IP") 131 | flags.String("port", "9932", "Set server PORT") 132 | 133 | // Load config file 134 | viper.SetConfigName("tidalwave") 135 | viper.SetEnvPrefix("tidalwave") 136 | viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) 137 | viper.SetConfigType("json") 138 | viper.AddConfigPath(".") 139 | viper.AddConfigPath("/etc") 140 | viper.AddConfigPath("$HOME/.tidalwave") 141 | 142 | flags.VisitAll(func(f *pflag.Flag) { 143 | err := viper.BindPFlag(f.Name, flags.Lookup(f.Name)) 144 | if err != nil { 145 | fmt.Println(err.Error()) 146 | } 147 | }) 148 | 149 | return rootCmd 150 | } 151 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/busbud/tidalwave 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/davecgh/go-spew v1.1.1 7 | github.com/dustinblackman/moment v0.0.0-20170412202417-fd1acf26c3c0 8 | github.com/golangci/golangci-lint v1.24.0 9 | github.com/jinzhu/copier v0.0.0-20180308034124-7e38e58719c3 10 | github.com/labstack/echo/v4 v4.1.10 11 | github.com/lfittl/pg_query_go v1.0.0 12 | github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983 13 | github.com/pelletier/go-toml v1.3.0 // indirect 14 | github.com/spf13/afero v1.2.2 // indirect 15 | github.com/spf13/cobra v0.0.5 16 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 17 | github.com/spf13/pflag v1.0.5 18 | github.com/spf13/viper v1.6.1 19 | github.com/tidwall/gjson v1.2.1 20 | github.com/tidwall/match v1.0.1 // indirect 21 | github.com/tidwall/pretty v0.0.0-20190325153808-1166b9ac2b65 // indirect 22 | github.com/ungerik/go-dry v0.0.0-20180411133923-654ae31114c8 23 | go.uber.org/zap v1.10.0 24 | ) 25 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 3 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 4 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 5 | github.com/OpenPeeDeeP/depguard v1.0.1 h1:VlW4R6jmBIv3/u1JNlawEvJMM4J+dPORPaZasQee8Us= 6 | github.com/OpenPeeDeeP/depguard v1.0.1/go.mod h1:xsIw86fROiiwelg+jB2uM9PiKihMMmUx/1V+TNhjQvM= 7 | github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= 8 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 9 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 10 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 11 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 12 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 13 | github.com/bombsimon/wsl/v2 v2.0.0 h1:+Vjcn+/T5lSrO8Bjzhk4v14Un/2UyCA1E3V5j9nwTkQ= 14 | github.com/bombsimon/wsl/v2 v2.0.0/go.mod h1:mf25kr/SqFEPhhcxW1+7pxzGlW+hIl/hYTKY95VwV8U= 15 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 16 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 17 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= 18 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 19 | github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= 20 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 21 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 22 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 23 | github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= 24 | github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= 25 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 26 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 27 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 28 | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= 29 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 30 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= 31 | github.com/dustinblackman/moment v0.0.0-20170412202417-fd1acf26c3c0 h1:q6rQ5kB8rVPnGu8c+SYZt0DBXZko1Vbz8mGA7wz4wzE= 32 | github.com/dustinblackman/moment v0.0.0-20170412202417-fd1acf26c3c0/go.mod h1:ponf1bbirf3z42GZfzlM67tF85moPzn/XWOr3P2KIwI= 33 | github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= 34 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 35 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 36 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 37 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 38 | github.com/go-critic/go-critic v0.4.1 h1:4DTQfT1wWwLg/hzxwD9bkdhDQrdJtxe6DUTadPlrIeE= 39 | github.com/go-critic/go-critic v0.4.1/go.mod h1:7/14rZGnZbY6E38VEGk2kVhoq6itzc1E68facVDK23g= 40 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 41 | github.com/go-lintpack/lintpack v0.5.2 h1:DI5mA3+eKdWeJ40nU4d6Wc26qmdG8RCi/btYq0TuRN0= 42 | github.com/go-lintpack/lintpack v0.5.2/go.mod h1:NwZuYi2nUHho8XEIZ6SIxihrnPoqBTDqfpXvXAN0sXM= 43 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 44 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 45 | github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8= 46 | github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= 47 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 48 | github.com/go-toolsmith/astcast v1.0.0 h1:JojxlmI6STnFVG9yOImLeGREv8W2ocNUM+iOhR6jE7g= 49 | github.com/go-toolsmith/astcast v1.0.0/go.mod h1:mt2OdQTeAQcY4DQgPSArJjHCcOwlX+Wl/kwN+LbLGQ4= 50 | github.com/go-toolsmith/astcopy v1.0.0 h1:OMgl1b1MEpjFQ1m5ztEO06rz5CUd3oBv9RF7+DyvdG8= 51 | github.com/go-toolsmith/astcopy v1.0.0/go.mod h1:vrgyG+5Bxrnz4MZWPF+pI4R8h3qKRjjyvV/DSez4WVQ= 52 | github.com/go-toolsmith/astequal v0.0.0-20180903214952-dcb477bfacd6 h1:aTBUNRTatDDU24gbOEKEoLiDwxtc98ga6K/iMTm6fvs= 53 | github.com/go-toolsmith/astequal v0.0.0-20180903214952-dcb477bfacd6/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY= 54 | github.com/go-toolsmith/astequal v1.0.0 h1:4zxD8j3JRFNyLN46lodQuqz3xdKSrur7U/sr0SDS/gQ= 55 | github.com/go-toolsmith/astequal v1.0.0/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY= 56 | github.com/go-toolsmith/astfmt v0.0.0-20180903215011-8f8ee99c3086 h1:EIMuvbE9fbtQtimdLe5yeXjuC5CeKbQt8zH6GwtIrhM= 57 | github.com/go-toolsmith/astfmt v0.0.0-20180903215011-8f8ee99c3086/go.mod h1:mP93XdblcopXwlyN4X4uodxXQhldPGZbcEJIimQHrkg= 58 | github.com/go-toolsmith/astfmt v1.0.0 h1:A0vDDXt+vsvLEdbMFJAUBI/uTbRw1ffOPnxsILnFL6k= 59 | github.com/go-toolsmith/astfmt v1.0.0/go.mod h1:cnWmsOAuq4jJY6Ct5YWlVLmcmLMn1JUPuQIHCY7CJDw= 60 | github.com/go-toolsmith/astinfo v0.0.0-20180906194353-9809ff7efb21/go.mod h1:dDStQCHtmZpYOmjRP/8gHHnCCch3Zz3oEgCdZVdtweU= 61 | github.com/go-toolsmith/astp v0.0.0-20180903215135-0af7e3c24f30 h1:zRJPftZJNLPDiOtvYbFRwjSbaJAcVOf80TeEmWGe2kQ= 62 | github.com/go-toolsmith/astp v0.0.0-20180903215135-0af7e3c24f30/go.mod h1:SV2ur98SGypH1UjcPpCatrV5hPazG6+IfNHbkDXBRrk= 63 | github.com/go-toolsmith/astp v1.0.0 h1:alXE75TXgcmupDsMK1fRAy0YUzLzqPVvBKoyWV+KPXg= 64 | github.com/go-toolsmith/astp v1.0.0/go.mod h1:RSyrtpVlfTFGDYRbrjyWP1pYu//tSFcvdYrA8meBmLI= 65 | github.com/go-toolsmith/pkgload v0.0.0-20181119091011-e9e65178eee8 h1:vVouagbdmqTVlCIAxpyYsNNTbkKZ3V66VpKOLU/s6W4= 66 | github.com/go-toolsmith/pkgload v0.0.0-20181119091011-e9e65178eee8/go.mod h1:WoMrjiy4zvdS+Bg6z9jZH82QXwkcgCBX6nOfnmdaHks= 67 | github.com/go-toolsmith/pkgload v1.0.0 h1:4DFWWMXVfbcN5So1sBNW9+yeiMqLFGl1wFLTL5R0Tgg= 68 | github.com/go-toolsmith/pkgload v1.0.0/go.mod h1:5eFArkbO80v7Z0kdngIxsRXRMTaX4Ilcwuh3clNrQJc= 69 | github.com/go-toolsmith/strparse v1.0.0 h1:Vcw78DnpCAKlM20kSbAyO4mPfJn/lyYA4BJUDxe2Jb4= 70 | github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8= 71 | github.com/go-toolsmith/typep v1.0.0 h1:zKymWyA1TRYvqYrYDrfEMZULyrhcnGY3x7LDKU2XQaA= 72 | github.com/go-toolsmith/typep v1.0.0/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU= 73 | github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= 74 | github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= 75 | github.com/gofrs/flock v0.0.0-20190320160742-5135e617513b h1:ekuhfTjngPhisSjOJ0QWKpPQE8/rbknHaes6WVJj5Hw= 76 | github.com/gofrs/flock v0.0.0-20190320160742-5135e617513b/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= 77 | github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo= 78 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 79 | github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= 80 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 81 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 82 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 83 | github.com/golang/mock v1.1.1 h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8= 84 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 85 | github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= 86 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 87 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 88 | github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= 89 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 90 | github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2 h1:23T5iq8rbUYlhpt5DB4XJkc6BU31uODLD1o1gKvZmD0= 91 | github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2/go.mod h1:k9Qvh+8juN+UKMCS/3jFtGICgW8O96FVaZsaxdzDkR4= 92 | github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a h1:w8hkcTqaFpzKqonE9uMCefW1WDie15eSP/4MssdenaM= 93 | github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a/go.mod h1:ryS0uhF+x9jgbj/N71xsEqODy9BN81/GonCZiOzirOk= 94 | github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6 h1:YYWNAGTKWhKpcLLt7aSj/odlKrSrelQwlovBpDuf19w= 95 | github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6/go.mod h1:DbHgvLiFKX1Sh2T1w8Q/h4NAI8MHIpzCdnBUDTXU3I0= 96 | github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613 h1:9kfjN3AdxcbsZBf8NjltjWihK2QfBBBZuv91cMFfDHw= 97 | github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613/go.mod h1:SyvUF2NxV+sN8upjjeVYr5W7tyxaT1JVtvhKhOn2ii8= 98 | github.com/golangci/goconst v0.0.0-20180610141641-041c5f2b40f3 h1:pe9JHs3cHHDQgOFXJJdYkK6fLz2PWyYtP4hthoCMvs8= 99 | github.com/golangci/goconst v0.0.0-20180610141641-041c5f2b40f3/go.mod h1:JXrF4TWy4tXYn62/9x8Wm/K/dm06p8tCKwFRDPZG/1o= 100 | github.com/golangci/gocyclo v0.0.0-20180528134321-2becd97e67ee h1:J2XAy40+7yz70uaOiMbNnluTg7gyQhtGqLQncQh+4J8= 101 | github.com/golangci/gocyclo v0.0.0-20180528134321-2becd97e67ee/go.mod h1:ozx7R9SIwqmqf5pRP90DhR2Oay2UIjGuKheCBCNwAYU= 102 | github.com/golangci/gofmt v0.0.0-20190930125516-244bba706f1a h1:iR3fYXUjHCR97qWS8ch1y9zPNsgXThGwjKPrYfqMPks= 103 | github.com/golangci/gofmt v0.0.0-20190930125516-244bba706f1a/go.mod h1:9qCChq59u/eW8im404Q2WWTrnBUQKjpNYKMbU4M7EFU= 104 | github.com/golangci/golangci-lint v1.24.0 h1:OcmSTTMPqI/VT4GvN1fKuE9NX15dDXIwolO0l08334U= 105 | github.com/golangci/golangci-lint v1.24.0/go.mod h1:yIqiAZ2SSQqg+1JeFlAdvEWjGVz4uu5jr4lrciqA1gE= 106 | github.com/golangci/ineffassign v0.0.0-20190609212857-42439a7714cc h1:gLLhTLMk2/SutryVJ6D4VZCU3CUqr8YloG7FPIBWFpI= 107 | github.com/golangci/ineffassign v0.0.0-20190609212857-42439a7714cc/go.mod h1:e5tpTHCfVze+7EpLEozzMB3eafxo2KT5veNg1k6byQU= 108 | github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0 h1:MfyDlzVjl1hoaPzPD4Gpb/QgoRfSBR0jdhwGyAWwMSA= 109 | github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0/go.mod h1:66R6K6P6VWk9I95jvqGxkqJxVWGFy9XlDwLwVz1RCFg= 110 | github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca h1:kNY3/svz5T29MYHubXix4aDDuE3RWHkPvopM/EDv/MA= 111 | github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca/go.mod h1:tvlJhZqDe4LMs4ZHD0oMUlt9G2LWuDGoisJTBzLMV9o= 112 | github.com/golangci/misspell v0.0.0-20180809174111-950f5d19e770 h1:EL/O5HGrF7Jaq0yNhBLucz9hTuRzj2LdwGBOaENgxIk= 113 | github.com/golangci/misspell v0.0.0-20180809174111-950f5d19e770/go.mod h1:dEbvlSfYbMQDtrpRMQU675gSDLDNa8sCPPChZ7PhiVA= 114 | github.com/golangci/prealloc v0.0.0-20180630174525-215b22d4de21 h1:leSNB7iYzLYSSx3J/s5sVf4Drkc68W2wm4Ixh/mr0us= 115 | github.com/golangci/prealloc v0.0.0-20180630174525-215b22d4de21/go.mod h1:tf5+bzsHdTM0bsB7+8mt0GUMvjCgwLpTapNZHU8AajI= 116 | github.com/golangci/revgrep v0.0.0-20180526074752-d9c87f5ffaf0 h1:HVfrLniijszjS1aiNg8JbBMO2+E1WIQ+j/gL4SQqGPg= 117 | github.com/golangci/revgrep v0.0.0-20180526074752-d9c87f5ffaf0/go.mod h1:qOQCunEYvmd/TLamH+7LlVccLvUH5kZNhbCgTHoBbp4= 118 | github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 h1:zwtduBRr5SSWhqsYNgcuWO2kFlpdOZbP0+yRjmvPGys= 119 | github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4/go.mod h1:Izgrg8RkN3rCIMLGE9CyYmU9pY2Jer6DgANEnZ/L/cQ= 120 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 121 | github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= 122 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 123 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 124 | github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 125 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= 126 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 127 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 128 | github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3 h1:JVnpOZS+qxli+rgVl98ILOXVNbW+kb5wcxeGx8ShUIw= 129 | github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE= 130 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 131 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 132 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 133 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 134 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 135 | github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= 136 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 137 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 138 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 139 | github.com/jingyugao/rowserrcheck v0.0.0-20191204022205-72ab7603b68a h1:GmsqmapfzSJkm28dhRoHz2tLRbJmqhU86IPgBtN3mmk= 140 | github.com/jingyugao/rowserrcheck v0.0.0-20191204022205-72ab7603b68a/go.mod h1:xRskid8CManxVta/ALEhJha/pweKBaVG6fWgc0yH25s= 141 | github.com/jinzhu/copier v0.0.0-20180308034124-7e38e58719c3 h1:sHsPfNMAG70QAvKbddQ0uScZCHQoZsT5NykGRCeeeIs= 142 | github.com/jinzhu/copier v0.0.0-20180308034124-7e38e58719c3/go.mod h1:yL958EeXv8Ylng6IfnvG4oflryUi3vgA3xPs9hmII1s= 143 | github.com/jirfag/go-printf-func-name v0.0.0-20191110105641-45db9963cdd3 h1:jNYPNLe3d8smommaoQlK7LOA5ESyUJJ+Wf79ZtA7Vp4= 144 | github.com/jirfag/go-printf-func-name v0.0.0-20191110105641-45db9963cdd3/go.mod h1:HEWGJkRDzjJY2sqdDwxccsGicWEf9BQOZsq2tV+xzM0= 145 | github.com/jmoiron/sqlx v1.2.1-0.20190826204134-d7d95172beb5/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= 146 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 147 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 148 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 149 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 150 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 151 | github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= 152 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 153 | github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= 154 | github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= 155 | github.com/klauspost/cpuid v0.0.0-20180405133222-e7e905edc00e/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= 156 | github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= 157 | github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= 158 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 159 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 160 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 161 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 162 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 163 | github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= 164 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 165 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 166 | github.com/labstack/echo/v4 v4.1.10 h1:/yhIpO50CBInUbE/nHJtGIyhBv0dJe2cDAYxc3V3uMo= 167 | github.com/labstack/echo/v4 v4.1.10/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g= 168 | github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0= 169 | github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= 170 | github.com/lfittl/pg_query_go v1.0.0 h1:rcHZK5DBEUoxtO6dACP+UVCHKtA1ZsELBW0rSjOXMAE= 171 | github.com/lfittl/pg_query_go v1.0.0/go.mod h1:jcikG62RKf+NIWmbLzjjk73m4x6um2pKf3h+TJyINms= 172 | github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 173 | github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 174 | github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= 175 | github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= 176 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 177 | github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= 178 | github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 179 | github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983 h1:wL11wNW7dhKIcRCHSm4sHKPWz0tt4mwBsVodG7+Xyqg= 180 | github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 181 | github.com/matoous/godox v0.0.0-20190911065817-5d6d842e92eb h1:RHba4YImhrUVQDHUCe2BNSOz4tVy2yGyXhvYDvxGgeE= 182 | github.com/matoous/godox v0.0.0-20190911065817-5d6d842e92eb/go.mod h1:1BELzlh859Sh1c6+90blK8lbYy0kwQf1bYlBhBysy1s= 183 | github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= 184 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 185 | github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= 186 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 187 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 188 | github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg= 189 | github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= 190 | github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= 191 | github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= 192 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 193 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 194 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 195 | github.com/mitchellh/go-ps v0.0.0-20190716172923-621e5597135b/go.mod h1:r1VsdOzOPt1ZSrGZWFoNhsAedKnEd6r9Np1+5blZCWk= 196 | github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= 197 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 198 | github.com/mozilla/tls-observatory v0.0.0-20190404164649-a3c1b6cfecfd/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk= 199 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 200 | github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d h1:AREM5mwr4u1ORQBMvzfzBgpsctsbQikCVpvC+tX285E= 201 | github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU= 202 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 203 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 204 | github.com/onsi/ginkgo v1.11.0 h1:JAKSXpt1YjtLA7YpPiqO9ss6sNXEsPfSGdwN0UHqzrw= 205 | github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 206 | github.com/onsi/gomega v1.8.1 h1:C5Dqfs/LeauYDX0jJXIe2SWmwCbGzx9yF8C8xy3Lh34= 207 | github.com/onsi/gomega v1.8.1/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= 208 | github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= 209 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 210 | github.com/pelletier/go-toml v1.3.0 h1:e5+lF2E4Y2WCIxBefVowBuB0iHrUH4HZ8q+6mGF7fJc= 211 | github.com/pelletier/go-toml v1.3.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo= 212 | github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= 213 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 214 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 215 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 216 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 217 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 218 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 219 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= 220 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 221 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 222 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 223 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 224 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 225 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 226 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= 227 | github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1:5STLWrekHfjyYwxBRVRXNOSewLJ3PWfDJd1VyTS21fI= 228 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 229 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 230 | github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 231 | github.com/securego/gosec v0.0.0-20200103095621-79fbf3af8d83 h1:AtnWoOvTioyDXFvu96MWEeE8qj4COSQnJogzLy/u41A= 232 | github.com/securego/gosec v0.0.0-20200103095621-79fbf3af8d83/go.mod h1:vvbZ2Ae7AzSq3/kywjUDxSNq2SJ27RxCz2un0H3ePqE= 233 | github.com/shirou/gopsutil v0.0.0-20190901111213-e4ec7b275ada/go.mod h1:WWnYX4lzhCH5h/3YBfyVA3VbLYjlMZZAQcW9ojMexNc= 234 | github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc= 235 | github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e h1:MZM7FHLqUHYI0Y/mQAt3d2aYa0SiNms/hFqC9qJYolM= 236 | github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= 237 | github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041 h1:llrF3Fs4018ePo4+G/HV/uQUqEI1HMDjCeOf2V6puPc= 238 | github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= 239 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 240 | github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= 241 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 242 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= 243 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 244 | github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= 245 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 246 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 247 | github.com/sourcegraph/go-diff v0.5.1 h1:gO6i5zugwzo1RVTvgvfwCOSVegNuvnNi6bAD1QCmkHs= 248 | github.com/sourcegraph/go-diff v0.5.1/go.mod h1:j2dHj3m8aZgQO8lMTcTnBcXkRRRqi34cd2MNlA9u1mE= 249 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 250 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 251 | github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= 252 | github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= 253 | github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= 254 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 255 | github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= 256 | github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= 257 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 258 | github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= 259 | github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= 260 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 261 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 262 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 263 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 264 | github.com/spf13/viper v1.3.2 h1:VUFqw5KcqRf7i70GOzW7N+Q7+gxVBkSSqiXB12+JQ4M= 265 | github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= 266 | github.com/spf13/viper v1.6.1 h1:VPZzIkznI1YhVMRi6vNFLHSwhnhReBfgTxIPccpfdZk= 267 | github.com/spf13/viper v1.6.1/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k= 268 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 269 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 270 | github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= 271 | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 272 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= 273 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 274 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 275 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 276 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 277 | github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= 278 | github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= 279 | github.com/tidwall/gjson v1.2.1 h1:j0efZLrZUvNerEf6xqoi0NjWMK5YlLrR7Guo/dxY174= 280 | github.com/tidwall/gjson v1.2.1/go.mod h1:c/nTNbUr0E0OrXEhq1pwa8iEgc2DOt4ZZqAt1HtCkPA= 281 | github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc= 282 | github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E= 283 | github.com/tidwall/pretty v0.0.0-20190325153808-1166b9ac2b65 h1:rQ229MBgvW68s1/g6f1/63TgYwYxfF4E+bi/KC19P8g= 284 | github.com/tidwall/pretty v0.0.0-20190325153808-1166b9ac2b65/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= 285 | github.com/timakin/bodyclose v0.0.0-20190930140734-f7f2e9bca95e h1:RumXZ56IrCj4CL+g1b9OL/oH0QnsF976bC8xQFYUD5Q= 286 | github.com/timakin/bodyclose v0.0.0-20190930140734-f7f2e9bca95e/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk= 287 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 288 | github.com/tommy-muehle/go-mnd v1.3.1-0.20200224220436-e6f9a994e8fa h1:RC4maTWLKKwb7p1cnoygsbKIgNlJqSYBeAFON3Ar8As= 289 | github.com/tommy-muehle/go-mnd v1.3.1-0.20200224220436-e6f9a994e8fa/go.mod h1:dSUh0FtTP8VhvkL1S+gUR1OKd9ZnSaozuI6r3m6wOig= 290 | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= 291 | github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= 292 | github.com/ultraware/funlen v0.0.2 h1:Av96YVBwwNSe4MLR7iI/BIa3VyI7/djnto/pK3Uxbdo= 293 | github.com/ultraware/funlen v0.0.2/go.mod h1:Dp4UiAus7Wdb9KUZsYWZEWiRzGuM2kXM1lPbfaF6xhA= 294 | github.com/ultraware/whitespace v0.0.4 h1:If7Va4cM03mpgrNH9k49/VOicWpGoG70XPBFFODYDsg= 295 | github.com/ultraware/whitespace v0.0.4/go.mod h1:aVMh/gQve5Maj9hQ/hg+F75lr/X5A89uZnzAmWSineA= 296 | github.com/ungerik/go-dry v0.0.0-20180411133923-654ae31114c8 h1:p6JzR5AMj5LyCEovRh5MOxmyuuwOEtfcVDwKqsBn41I= 297 | github.com/ungerik/go-dry v0.0.0-20180411133923-654ae31114c8/go.mod h1:+LeLocciSarKa1pxOY7gmBQ7dSk5nB1w1f3nvvLw0j0= 298 | github.com/uudashr/gocognit v1.0.1 h1:MoG2fZ0b/Eo7NXoIwCVFLG5JED3qgQz5/NEE+rOsjPs= 299 | github.com/uudashr/gocognit v1.0.1/go.mod h1:j44Ayx2KW4+oB6SWMv8KsmHzZrOInQav7D3cQMJ5JUM= 300 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 301 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 302 | github.com/valyala/fasthttp v1.2.0/go.mod h1:4vX61m6KN+xDduDNwXrhIAVZaZaZiQ1luJk8LWSxF3s= 303 | github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8= 304 | github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= 305 | github.com/valyala/quicktemplate v1.2.0/go.mod h1:EH+4AkTd43SvgIbQHYu59/cJyxDoOVRUAfrukLPuGJ4= 306 | github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= 307 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 308 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 309 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 310 | go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU= 311 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 312 | go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= 313 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 314 | go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM= 315 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 316 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 317 | golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 318 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 319 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 320 | golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc= 321 | golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 322 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= 323 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 324 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 325 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 326 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 327 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee h1:WG0RUwxtNT4qqaXX3DPA8zHFNm/D9xaBpxzHt1WcA/E= 328 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 329 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 330 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 331 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 332 | golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 333 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 334 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 335 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 336 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 337 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= 338 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 339 | golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 340 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 341 | golang.org/x/net v0.0.0-20190923162816-aa69164e4478 h1:l5EDrHhldLYb3ZRHDUhXF7Om7MvYXnkV9/iQNo1lX6g= 342 | golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 343 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 344 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= 345 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 346 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 347 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 348 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 349 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 350 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 351 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 352 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 353 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 354 | golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 355 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 356 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 357 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 358 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 359 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= 360 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 361 | golang.org/x/sys v0.0.0-20190922100055-0a153f010e69 h1:rOhMmluY6kLMhdnrivzec6lLgaVbMHMn2ISQXJeJ5EM= 362 | golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 363 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 364 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 365 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 366 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 367 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 368 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 369 | golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 370 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 371 | golang.org/x/tools v0.0.0-20181117154741-2ddaf7f79a09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 372 | golang.org/x/tools v0.0.0-20190110163146-51295c7ec13a/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 373 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 374 | golang.org/x/tools v0.0.0-20190221204921-83362c3779f5/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 375 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 376 | golang.org/x/tools v0.0.0-20190311215038-5c2858a9cfe5/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 377 | golang.org/x/tools v0.0.0-20190322203728-c1a832b0ad89/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 378 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384 h1:TFlARGu6Czu1z7q93HTxcP1P+/ZFC/IKythI5RzrnRg= 379 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 380 | golang.org/x/tools v0.0.0-20190521203540-521d6ed310dd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 381 | golang.org/x/tools v0.0.0-20190719005602-e377ae9d6386/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= 382 | golang.org/x/tools v0.0.0-20190910044552-dd2b5c81c578/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 383 | golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 384 | golang.org/x/tools v0.0.0-20191113232020-e2727e816f5a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 385 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 386 | golang.org/x/tools v0.0.0-20200102140908-9497f49d5709/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 387 | golang.org/x/tools v0.0.0-20200204192400-7124308813f3 h1:Ms82wn6YK4ZycO6Bxyh0kxX3gFFVGo79CCuc52xgcys= 388 | golang.org/x/tools v0.0.0-20200204192400-7124308813f3/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 389 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 390 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 h1:/atklqdjdhuosWIl6AIbOeHJjicWYPqR9bpxqxYG2pA= 391 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 392 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 393 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 394 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 395 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 396 | google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 397 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 398 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 399 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 400 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 401 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 402 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 403 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 404 | gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= 405 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 406 | gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= 407 | gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 408 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 409 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 410 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 411 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 412 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 413 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 414 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 415 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 416 | gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 417 | gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= 418 | gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 419 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 420 | honnef.co/go/tools v0.0.1-2020.1.3 h1:sXmLre5bzIR6ypkjXCDI3jHPssRhc8KD/Ome589sc3U= 421 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 422 | mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed h1:WX1yoOaKQfddO/mLzdV4wptyWgoH/6hwLs7QHTixo0I= 423 | mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc= 424 | mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b h1:DxJ5nJdkhDlLok9K6qO+5290kphDJbHOQO1DFFFTeBo= 425 | mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4= 426 | mvdan.cc/unparam v0.0.0-20190720180237-d51796306d8f h1:Cq7MalBHYACRd6EesksG1Q8EoIAKOsiZviGKbOLIej4= 427 | mvdan.cc/unparam v0.0.0-20190720180237-d51796306d8f/go.mod h1:4G1h5nDURzA3bwVMZIVpwbkw+04kSxk3rAtzlimaUJw= 428 | sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4 h1:JPJh2pk3+X4lXAkZIk2RuE/7/FoK9maXw+TNPJhVS/c= 429 | sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= 430 | -------------------------------------------------------------------------------- /go.tools.go: -------------------------------------------------------------------------------- 1 | // +build tools 2 | 3 | package main 4 | 5 | import ( 6 | _ "github.com/golangci/golangci-lint/cmd/golangci-lint" 7 | _ "github.com/mailru/easyjson/easyjson" 8 | ) 9 | -------------------------------------------------------------------------------- /logger/logger.go: -------------------------------------------------------------------------------- 1 | // Package logger initializes and stores a Zap logger instance in global. 2 | package logger 3 | 4 | import "go.uber.org/zap" 5 | 6 | // Log is the initialized logger that can be used in other modules 7 | var Log *zap.SugaredLogger 8 | 9 | // Init setups the logger and stores it in global 10 | func Init(debug bool) { 11 | var initLogger *zap.Logger 12 | if debug { 13 | initLogger, _ = zap.NewDevelopment() 14 | } else { 15 | initLogger, _ = zap.NewProduction() 16 | } 17 | Log = initLogger.Sugar() 18 | } 19 | -------------------------------------------------------------------------------- /parser/count-distinct.go: -------------------------------------------------------------------------------- 1 | // Package parser handles parsing log files based on the SQL execution type. 2 | package parser 3 | 4 | import ( 5 | "sync" 6 | 7 | "github.com/busbud/tidalwave/logger" 8 | "github.com/busbud/tidalwave/sqlquery" 9 | "github.com/tidwall/gjson" 10 | ) 11 | 12 | func distinctCountParse(query *sqlquery.QueryParams, resultsChan chan<- map[string]int, logPath string, wg *sync.WaitGroup) { 13 | defer wg.Done() 14 | 15 | results := map[string]int{} 16 | err := readLines(logPath, func(line *[]byte) { 17 | if query.ProcessLine(line) { 18 | res := gjson.GetBytes(*line, query.AggrPath) 19 | if res.Type != 0 { 20 | value := res.String() 21 | results[value]++ 22 | } 23 | } 24 | }) 25 | 26 | if err != nil { 27 | logger.Log.Fatal(err) 28 | } 29 | 30 | resultsChan <- results 31 | } 32 | 33 | // CountDistinct executes a COUNT(DISTINCT()) query over log results. 34 | // SELECT COUNT(DISTINCT(line.cmd)) FROM testapp WHERE date > '2016-10-05' 35 | func (tp *TidalwaveParser) CountDistinct() *map[string]int { //nolint:gocritic // Leave it alone. 36 | logsLen := len(tp.LogPaths) 37 | resultsChan := make(chan map[string]int, logsLen) 38 | 39 | var wg sync.WaitGroup 40 | wg.Add(logsLen + 1) 41 | 42 | results := []map[string]int{} 43 | coreLimit := make(chan bool, tp.MaxParallelism) 44 | go func() { 45 | for res := range resultsChan { 46 | results = append(results, res) 47 | <-coreLimit 48 | if len(results) == logsLen { 49 | wg.Done() 50 | } 51 | } 52 | }() 53 | 54 | for i := 0; i < logsLen; i++ { 55 | go distinctCountParse(tp.Query, resultsChan, tp.LogPaths[i], &wg) 56 | coreLimit <- true 57 | } 58 | 59 | wg.Wait() 60 | 61 | mergedResults := map[string]int{} 62 | for idx := range results { 63 | for key, val := range results[idx] { 64 | mergedResults[key] += val 65 | } 66 | } 67 | 68 | results = nil // Manual GC 69 | return &mergedResults 70 | } 71 | -------------------------------------------------------------------------------- /parser/count.go: -------------------------------------------------------------------------------- 1 | // Package parser handles parsing log files based on the SQL execution type. 2 | package parser 3 | 4 | import ( 5 | "sync" 6 | 7 | "github.com/busbud/tidalwave/logger" 8 | "github.com/busbud/tidalwave/sqlquery" 9 | ) 10 | 11 | func countParse(query *sqlquery.QueryParams, resultsChan chan<- int, logPath string, wg *sync.WaitGroup) { 12 | defer wg.Done() 13 | 14 | count := 0 15 | err := readLines(logPath, func(line *[]byte) { 16 | if query.ProcessLine(line) { 17 | count++ 18 | } 19 | }) 20 | 21 | if err != nil { 22 | logger.Log.Fatal(err) 23 | } 24 | 25 | resultsChan <- count 26 | } 27 | 28 | // Count executes a COUNT() query over log results. 29 | // SELECT COUNT(*) FROM testapp WHERE date > '2016-10-05' 30 | func (tp *TidalwaveParser) Count() int { 31 | logsLen := len(tp.LogPaths) 32 | resultsChan := make(chan int, logsLen) 33 | 34 | var wg sync.WaitGroup 35 | wg.Add(logsLen + 1) 36 | 37 | counts := []int{} 38 | coreLimit := make(chan bool, tp.MaxParallelism) 39 | go func() { 40 | for res := range resultsChan { 41 | counts = append(counts, res) 42 | <-coreLimit 43 | if len(counts) == logsLen { 44 | wg.Done() 45 | } 46 | } 47 | }() 48 | 49 | for i := 0; i < logsLen; i++ { 50 | go countParse(tp.Query, resultsChan, tp.LogPaths[i], &wg) 51 | coreLimit <- true 52 | } 53 | 54 | wg.Wait() 55 | 56 | sum := 0 57 | for _, count := range counts { 58 | sum += count 59 | } 60 | 61 | return sum 62 | } 63 | -------------------------------------------------------------------------------- /parser/distinct.go: -------------------------------------------------------------------------------- 1 | // Package parser handles parsing log files based on the SQL execution type. 2 | package parser 3 | 4 | import "sort" 5 | 6 | // Distinct executes a DISTINCT() query over log results. 7 | // SELECT DISTINCT(line.cmd) FROM testapp WHERE date > '2016-10-05' 8 | func (tp *TidalwaveParser) Distinct() *[]string { 9 | keys := []string{} 10 | for key := range *tp.CountDistinct() { 11 | keys = append(keys, key) 12 | } 13 | 14 | sort.Strings(keys) 15 | return &keys 16 | } 17 | -------------------------------------------------------------------------------- /parser/parser.go: -------------------------------------------------------------------------------- 1 | // Package parser handles parsing log files based on the SQL execution type. 2 | package parser 3 | 4 | import ( 5 | "bufio" 6 | "io" 7 | "os" 8 | "path" 9 | "path/filepath" 10 | "strings" 11 | "time" 12 | 13 | "github.com/busbud/tidalwave/logger" 14 | "github.com/busbud/tidalwave/sqlquery" 15 | "github.com/dustinblackman/moment" 16 | "github.com/spf13/viper" 17 | ) 18 | 19 | const ( 20 | fileDateFormat = "YYYY-MM-DDTHH-mm-ss" 21 | folderDateFormat = "YYYY-MM-DD" 22 | ) 23 | 24 | // TidalwaveParser does stuff 25 | type TidalwaveParser struct { 26 | MaxParallelism int 27 | LogPaths []string 28 | Query *sqlquery.QueryParams 29 | } 30 | 31 | // ChannelResults returns array results through a channel 32 | type ChannelResults struct { 33 | Type string 34 | Channel chan []byte 35 | } 36 | 37 | // ArrayResults does stuff 38 | //easyjson:json 39 | type ArrayResults struct { 40 | Type string `json:"type"` 41 | Results *[]string `json:"results"` 42 | } 43 | 44 | // IntResults does stuff 45 | //easyjson:json 46 | type IntResults struct { 47 | Type string `json:"type"` 48 | Results int `json:"results"` 49 | } 50 | 51 | // ObjectResults does stuff 52 | //easyjson:json 53 | type ObjectResults struct { 54 | Type string `json:"type"` 55 | Results *map[string]int `json:"results"` 56 | } 57 | 58 | func readLines(logPath string, callback func(*[]byte)) error { 59 | var err error 60 | 61 | maxAttemptes := 5 62 | retry := 0 63 | for retry < maxAttemptes { 64 | var file *os.File 65 | file, err = os.Open(logPath) 66 | if err != nil { 67 | retry++ 68 | waitTime := 30 * retry 69 | logger.Log.Debugf("Failed to open %s after %v/%v attempts, retrying in %v seconds. %s", logPath, retry, maxAttemptes, waitTime, err.Error()) 70 | time.Sleep(time.Duration(waitTime) * time.Second) 71 | continue 72 | } 73 | 74 | defer file.Close() //nolint:errcheck // Don't care if there's errors. 75 | 76 | reader := bufio.NewReader(file) 77 | delim := byte('\n') 78 | 79 | for { 80 | var line []byte 81 | line, err = reader.ReadBytes(delim) 82 | 83 | if err == io.EOF { 84 | retry = 100 85 | err = nil 86 | break 87 | } 88 | 89 | if err != nil { 90 | if strings.Contains(err.Error(), "input/output error") { 91 | retry++ 92 | waitTime := 30 * retry 93 | logger.Log.Debugf("Input/output error for %s after %v/%v attempts, retrying in %v seconds. %s", logPath, retry, maxAttemptes, waitTime, err.Error()) 94 | time.Sleep(time.Duration(waitTime) * time.Second) 95 | break 96 | } else { 97 | return err 98 | } 99 | } 100 | 101 | callback(&line) 102 | } 103 | } 104 | 105 | return err 106 | } 107 | 108 | func dateMatch(date *moment.Moment, dates []sqlquery.DateParam, dateOnly bool) bool { 109 | for idx := range dates { 110 | if !sqlquery.ProcessDate(&dates[idx], *date, dateOnly) { 111 | return false 112 | } 113 | } 114 | 115 | return true 116 | } 117 | 118 | // GetLogPathsForApp returns all log paths matching a query for a specified app 119 | func GetLogPathsForApp(query *sqlquery.QueryParams, appName, logRoot string) []string { 120 | var logPaths []string 121 | folderGlob, _ := filepath.Glob(path.Join(logRoot, appName+"/*/")) 122 | 123 | // TODO This can be optimized for single date queries. 124 | for _, folderPath := range folderGlob { 125 | folderDate := moment.New().Moment(folderDateFormat, path.Base(folderPath)) 126 | if dateMatch(folderDate, query.Dates, true) { 127 | globLogs, _ := filepath.Glob(path.Join(folderPath, "/*.log")) 128 | 129 | for _, filename := range globLogs { 130 | logDate := moment.New().Moment(fileDateFormat, strings.TrimSuffix(path.Base(filename), filepath.Ext(filename))) 131 | if dateMatch(logDate, query.Dates, false) { 132 | logPaths = append(logPaths, filename) 133 | } 134 | } 135 | } 136 | } 137 | 138 | return logPaths 139 | } 140 | 141 | // GetLogPaths returns all log paths matching a query 142 | func GetLogPaths(query *sqlquery.QueryParams, logRoot string) []string { 143 | var logPaths []string 144 | for _, appName := range query.From { 145 | logPaths = append(logPaths, GetLogPathsForApp(query, appName, logRoot)...) 146 | } 147 | 148 | return logPaths 149 | } 150 | 151 | // Query executes a given query string. 152 | func Query(queryString string) interface{} { 153 | query := sqlquery.New(queryString) 154 | logPaths := GetLogPaths(query, viper.GetString("logroot")) 155 | parser := TidalwaveParser{ 156 | MaxParallelism: viper.GetInt("max-parallelism"), 157 | LogPaths: logPaths, 158 | Query: query, 159 | } 160 | 161 | logger.Log.Debugf("Log Paths: %s", logPaths) 162 | 163 | // TODO: Add execution time to results. 164 | // TODO: Need to handle nil. 165 | switch query.Type { 166 | case sqlquery.TypeCountDistinct: 167 | return ObjectResults{sqlquery.TypeCountDistinct, parser.CountDistinct()} 168 | case sqlquery.TypeDistinct: 169 | return ArrayResults{sqlquery.TypeDistinct, parser.Distinct()} 170 | case sqlquery.TypeCount: 171 | return IntResults{sqlquery.TypeCount, parser.Count()} 172 | case sqlquery.TypeSearch: 173 | return ChannelResults{sqlquery.TypeSearch, parser.Search()} 174 | default: 175 | return nil 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /parser/parser_easyjson.go: -------------------------------------------------------------------------------- 1 | // Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT. 2 | 3 | package parser 4 | 5 | import ( 6 | json "encoding/json" 7 | easyjson "github.com/mailru/easyjson" 8 | jlexer "github.com/mailru/easyjson/jlexer" 9 | jwriter "github.com/mailru/easyjson/jwriter" 10 | ) 11 | 12 | // suppress unused package warning 13 | var ( 14 | _ *json.RawMessage 15 | _ *jlexer.Lexer 16 | _ *jwriter.Writer 17 | _ easyjson.Marshaler 18 | ) 19 | 20 | func easyjsonF59a38b1DecodeGithubCombusbudTidalwaveParser(in *jlexer.Lexer, out *ObjectResults) { 21 | isTopLevel := in.IsStart() 22 | if in.IsNull() { 23 | if isTopLevel { 24 | in.Consumed() 25 | } 26 | in.Skip() 27 | return 28 | } 29 | in.Delim('{') 30 | for !in.IsDelim('}') { 31 | key := in.UnsafeString() 32 | in.WantColon() 33 | if in.IsNull() { 34 | in.Skip() 35 | in.WantComma() 36 | continue 37 | } 38 | switch key { 39 | case "type": 40 | out.Type = string(in.String()) 41 | case "results": 42 | if in.IsNull() { 43 | in.Skip() 44 | out.Results = nil 45 | } else { 46 | if out.Results == nil { 47 | out.Results = new(map[string]int) 48 | } 49 | if in.IsNull() { 50 | in.Skip() 51 | } else { 52 | in.Delim('{') 53 | if !in.IsDelim('}') { 54 | *out.Results = make(map[string]int) 55 | } else { 56 | *out.Results = nil 57 | } 58 | for !in.IsDelim('}') { 59 | key := string(in.String()) 60 | in.WantColon() 61 | var v1 int 62 | v1 = int(in.Int()) 63 | (*out.Results)[key] = v1 64 | in.WantComma() 65 | } 66 | in.Delim('}') 67 | } 68 | } 69 | default: 70 | in.SkipRecursive() 71 | } 72 | in.WantComma() 73 | } 74 | in.Delim('}') 75 | if isTopLevel { 76 | in.Consumed() 77 | } 78 | } 79 | func easyjsonF59a38b1EncodeGithubCombusbudTidalwaveParser(out *jwriter.Writer, in ObjectResults) { 80 | out.RawByte('{') 81 | first := true 82 | _ = first 83 | { 84 | const prefix string = ",\"type\":" 85 | if first { 86 | first = false 87 | out.RawString(prefix[1:]) 88 | } else { 89 | out.RawString(prefix) 90 | } 91 | out.String(string(in.Type)) 92 | } 93 | { 94 | const prefix string = ",\"results\":" 95 | if first { 96 | first = false 97 | out.RawString(prefix[1:]) 98 | } else { 99 | out.RawString(prefix) 100 | } 101 | if in.Results == nil { 102 | out.RawString("null") 103 | } else { 104 | if *in.Results == nil && (out.Flags&jwriter.NilMapAsEmpty) == 0 { 105 | out.RawString(`null`) 106 | } else { 107 | out.RawByte('{') 108 | v2First := true 109 | for v2Name, v2Value := range *in.Results { 110 | if v2First { 111 | v2First = false 112 | } else { 113 | out.RawByte(',') 114 | } 115 | out.String(string(v2Name)) 116 | out.RawByte(':') 117 | out.Int(int(v2Value)) 118 | } 119 | out.RawByte('}') 120 | } 121 | } 122 | } 123 | out.RawByte('}') 124 | } 125 | 126 | // MarshalJSON supports json.Marshaler interface 127 | func (v ObjectResults) MarshalJSON() ([]byte, error) { 128 | w := jwriter.Writer{} 129 | easyjsonF59a38b1EncodeGithubCombusbudTidalwaveParser(&w, v) 130 | return w.Buffer.BuildBytes(), w.Error 131 | } 132 | 133 | // MarshalEasyJSON supports easyjson.Marshaler interface 134 | func (v ObjectResults) MarshalEasyJSON(w *jwriter.Writer) { 135 | easyjsonF59a38b1EncodeGithubCombusbudTidalwaveParser(w, v) 136 | } 137 | 138 | // UnmarshalJSON supports json.Unmarshaler interface 139 | func (v *ObjectResults) UnmarshalJSON(data []byte) error { 140 | r := jlexer.Lexer{Data: data} 141 | easyjsonF59a38b1DecodeGithubCombusbudTidalwaveParser(&r, v) 142 | return r.Error() 143 | } 144 | 145 | // UnmarshalEasyJSON supports easyjson.Unmarshaler interface 146 | func (v *ObjectResults) UnmarshalEasyJSON(l *jlexer.Lexer) { 147 | easyjsonF59a38b1DecodeGithubCombusbudTidalwaveParser(l, v) 148 | } 149 | func easyjsonF59a38b1DecodeGithubCombusbudTidalwaveParser1(in *jlexer.Lexer, out *IntResults) { 150 | isTopLevel := in.IsStart() 151 | if in.IsNull() { 152 | if isTopLevel { 153 | in.Consumed() 154 | } 155 | in.Skip() 156 | return 157 | } 158 | in.Delim('{') 159 | for !in.IsDelim('}') { 160 | key := in.UnsafeString() 161 | in.WantColon() 162 | if in.IsNull() { 163 | in.Skip() 164 | in.WantComma() 165 | continue 166 | } 167 | switch key { 168 | case "type": 169 | out.Type = string(in.String()) 170 | case "results": 171 | out.Results = int(in.Int()) 172 | default: 173 | in.SkipRecursive() 174 | } 175 | in.WantComma() 176 | } 177 | in.Delim('}') 178 | if isTopLevel { 179 | in.Consumed() 180 | } 181 | } 182 | func easyjsonF59a38b1EncodeGithubCombusbudTidalwaveParser1(out *jwriter.Writer, in IntResults) { 183 | out.RawByte('{') 184 | first := true 185 | _ = first 186 | { 187 | const prefix string = ",\"type\":" 188 | if first { 189 | first = false 190 | out.RawString(prefix[1:]) 191 | } else { 192 | out.RawString(prefix) 193 | } 194 | out.String(string(in.Type)) 195 | } 196 | { 197 | const prefix string = ",\"results\":" 198 | if first { 199 | first = false 200 | out.RawString(prefix[1:]) 201 | } else { 202 | out.RawString(prefix) 203 | } 204 | out.Int(int(in.Results)) 205 | } 206 | out.RawByte('}') 207 | } 208 | 209 | // MarshalJSON supports json.Marshaler interface 210 | func (v IntResults) MarshalJSON() ([]byte, error) { 211 | w := jwriter.Writer{} 212 | easyjsonF59a38b1EncodeGithubCombusbudTidalwaveParser1(&w, v) 213 | return w.Buffer.BuildBytes(), w.Error 214 | } 215 | 216 | // MarshalEasyJSON supports easyjson.Marshaler interface 217 | func (v IntResults) MarshalEasyJSON(w *jwriter.Writer) { 218 | easyjsonF59a38b1EncodeGithubCombusbudTidalwaveParser1(w, v) 219 | } 220 | 221 | // UnmarshalJSON supports json.Unmarshaler interface 222 | func (v *IntResults) UnmarshalJSON(data []byte) error { 223 | r := jlexer.Lexer{Data: data} 224 | easyjsonF59a38b1DecodeGithubCombusbudTidalwaveParser1(&r, v) 225 | return r.Error() 226 | } 227 | 228 | // UnmarshalEasyJSON supports easyjson.Unmarshaler interface 229 | func (v *IntResults) UnmarshalEasyJSON(l *jlexer.Lexer) { 230 | easyjsonF59a38b1DecodeGithubCombusbudTidalwaveParser1(l, v) 231 | } 232 | func easyjsonF59a38b1DecodeGithubCombusbudTidalwaveParser2(in *jlexer.Lexer, out *ArrayResults) { 233 | isTopLevel := in.IsStart() 234 | if in.IsNull() { 235 | if isTopLevel { 236 | in.Consumed() 237 | } 238 | in.Skip() 239 | return 240 | } 241 | in.Delim('{') 242 | for !in.IsDelim('}') { 243 | key := in.UnsafeString() 244 | in.WantColon() 245 | if in.IsNull() { 246 | in.Skip() 247 | in.WantComma() 248 | continue 249 | } 250 | switch key { 251 | case "type": 252 | out.Type = string(in.String()) 253 | case "results": 254 | if in.IsNull() { 255 | in.Skip() 256 | out.Results = nil 257 | } else { 258 | if out.Results == nil { 259 | out.Results = new([]string) 260 | } 261 | if in.IsNull() { 262 | in.Skip() 263 | *out.Results = nil 264 | } else { 265 | in.Delim('[') 266 | if *out.Results == nil { 267 | if !in.IsDelim(']') { 268 | *out.Results = make([]string, 0, 4) 269 | } else { 270 | *out.Results = []string{} 271 | } 272 | } else { 273 | *out.Results = (*out.Results)[:0] 274 | } 275 | for !in.IsDelim(']') { 276 | var v3 string 277 | v3 = string(in.String()) 278 | *out.Results = append(*out.Results, v3) 279 | in.WantComma() 280 | } 281 | in.Delim(']') 282 | } 283 | } 284 | default: 285 | in.SkipRecursive() 286 | } 287 | in.WantComma() 288 | } 289 | in.Delim('}') 290 | if isTopLevel { 291 | in.Consumed() 292 | } 293 | } 294 | func easyjsonF59a38b1EncodeGithubCombusbudTidalwaveParser2(out *jwriter.Writer, in ArrayResults) { 295 | out.RawByte('{') 296 | first := true 297 | _ = first 298 | { 299 | const prefix string = ",\"type\":" 300 | if first { 301 | first = false 302 | out.RawString(prefix[1:]) 303 | } else { 304 | out.RawString(prefix) 305 | } 306 | out.String(string(in.Type)) 307 | } 308 | { 309 | const prefix string = ",\"results\":" 310 | if first { 311 | first = false 312 | out.RawString(prefix[1:]) 313 | } else { 314 | out.RawString(prefix) 315 | } 316 | if in.Results == nil { 317 | out.RawString("null") 318 | } else { 319 | if *in.Results == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { 320 | out.RawString("null") 321 | } else { 322 | out.RawByte('[') 323 | for v4, v5 := range *in.Results { 324 | if v4 > 0 { 325 | out.RawByte(',') 326 | } 327 | out.String(string(v5)) 328 | } 329 | out.RawByte(']') 330 | } 331 | } 332 | } 333 | out.RawByte('}') 334 | } 335 | 336 | // MarshalJSON supports json.Marshaler interface 337 | func (v ArrayResults) MarshalJSON() ([]byte, error) { 338 | w := jwriter.Writer{} 339 | easyjsonF59a38b1EncodeGithubCombusbudTidalwaveParser2(&w, v) 340 | return w.Buffer.BuildBytes(), w.Error 341 | } 342 | 343 | // MarshalEasyJSON supports easyjson.Marshaler interface 344 | func (v ArrayResults) MarshalEasyJSON(w *jwriter.Writer) { 345 | easyjsonF59a38b1EncodeGithubCombusbudTidalwaveParser2(w, v) 346 | } 347 | 348 | // UnmarshalJSON supports json.Unmarshaler interface 349 | func (v *ArrayResults) UnmarshalJSON(data []byte) error { 350 | r := jlexer.Lexer{Data: data} 351 | easyjsonF59a38b1DecodeGithubCombusbudTidalwaveParser2(&r, v) 352 | return r.Error() 353 | } 354 | 355 | // UnmarshalEasyJSON supports easyjson.Unmarshaler interface 356 | func (v *ArrayResults) UnmarshalEasyJSON(l *jlexer.Lexer) { 357 | easyjsonF59a38b1DecodeGithubCombusbudTidalwaveParser2(l, v) 358 | } 359 | -------------------------------------------------------------------------------- /parser/search.go: -------------------------------------------------------------------------------- 1 | // Package parser handles parsing log files based on the SQL execution type. 2 | package parser 3 | 4 | import ( 5 | "strings" 6 | "sync" 7 | 8 | "github.com/busbud/tidalwave/logger" 9 | "github.com/busbud/tidalwave/sqlquery" 10 | "github.com/spf13/viper" 11 | "github.com/tidwall/gjson" 12 | ) 13 | 14 | // LogQueryStruct contains all information about a log file, including the matching entries to the query. 15 | type LogQueryStruct struct { 16 | LogPath string 17 | LineNumbers [][]int 18 | } 19 | 20 | func formatLine(query *sqlquery.QueryParams, line []byte) []byte { 21 | // If there were select statements, join those in to a smaller JSON object. 22 | if len(query.Selects) > 0 { 23 | selectedEntries := []string{} 24 | for idx, res := range gjson.GetManyBytes(line, query.Selects...) { 25 | keyName := "" 26 | for _, queryParam := range query.Queries { 27 | if queryParam.KeyPath == query.Selects[idx] && queryParam.KeyName != "" { 28 | keyName = queryParam.KeyName 29 | break 30 | } 31 | } 32 | 33 | if res.Type == gjson.Number || res.Type == gjson.JSON { 34 | selectedEntries = append(selectedEntries, `"`+keyName+`":`+res.String()) 35 | } else if res.Type == gjson.True { 36 | selectedEntries = append(selectedEntries, `"`+keyName+`":true`) 37 | } else if res.Type == gjson.False { 38 | selectedEntries = append(selectedEntries, `"`+keyName+`":false`) 39 | } else if res.Type == gjson.Null { 40 | selectedEntries = append(selectedEntries, `"`+keyName+`":null`) 41 | } else { 42 | selectedEntries = append(selectedEntries, `"`+keyName+`":"`+strings.ReplaceAll(res.String(), `"`, `\"`)+`"`) 43 | } 44 | } 45 | 46 | return []byte("{" + strings.Join(selectedEntries, ",") + "}") 47 | } 48 | 49 | return line 50 | } 51 | 52 | func searchParse(query *sqlquery.QueryParams, logStruct *LogQueryStruct, coreLimit <-chan bool, submitChannel chan<- []byte, wg *sync.WaitGroup) { 53 | defer wg.Done() 54 | 55 | logger.Log.Debugf("Processing: %s", logStruct.LogPath) 56 | lineNumber := -1 57 | lastLineNumber := -1 58 | 59 | err := readLines(logStruct.LogPath, func(line *[]byte) { 60 | lineNumber++ 61 | 62 | if query.ProcessLine(line) { 63 | if viper.GetBool("skip-sort") { 64 | submitChannel <- formatLine(query, *line) 65 | return 66 | } 67 | 68 | if lineNumber == (lastLineNumber+1) && lineNumber != 0 { 69 | logStruct.LineNumbers[len(logStruct.LineNumbers)-1][1] = lineNumber 70 | } else { 71 | logStruct.LineNumbers = append(logStruct.LineNumbers, []int{lineNumber, lineNumber}) 72 | } 73 | lastLineNumber = lineNumber 74 | } 75 | }) 76 | 77 | if err != nil { 78 | logger.Log.Fatal(err) 79 | } 80 | 81 | <-coreLimit 82 | } 83 | 84 | func searchSubmit(query *sqlquery.QueryParams, logStruct *LogQueryStruct, submitChannel chan<- []byte) { 85 | lineNumber := -1 86 | err := readLines(logStruct.LogPath, func(line *[]byte) { 87 | lineNumber++ 88 | acceptLine := false 89 | // TODO: Can this be better? Faster? 90 | for _, lineRange := range logStruct.LineNumbers { 91 | if lineNumber >= lineRange[0] && lineNumber <= lineRange[1] { 92 | acceptLine = true 93 | break 94 | } 95 | } 96 | 97 | if acceptLine { 98 | submitChannel <- formatLine(query, *line) 99 | } 100 | }) 101 | 102 | if err != nil { 103 | logger.Log.Fatal(err) 104 | } 105 | } 106 | 107 | // Search executes a normal match query over log results. 108 | // SELECT * FROM testapp WHERE date > '2016-10-05' 109 | func (tp *TidalwaveParser) Search() chan []byte { 110 | var wg sync.WaitGroup 111 | logsLen := len(tp.LogPaths) 112 | wg.Add(logsLen) 113 | 114 | submitChannel := make(chan []byte, 10000) 115 | go func() { 116 | coreLimit := make(chan bool, tp.MaxParallelism) 117 | logs := make([]LogQueryStruct, logsLen) 118 | for idx, logPath := range tp.LogPaths { 119 | logs[idx] = LogQueryStruct{LogPath: logPath} 120 | go searchParse(tp.Query, &logs[idx], coreLimit, submitChannel, &wg) 121 | coreLimit <- true 122 | } 123 | 124 | wg.Wait() 125 | 126 | if !viper.GetBool("skip-sort") { 127 | for idx := range logs { 128 | if len(logs[idx].LineNumbers) > 0 { 129 | searchSubmit(tp.Query, &logs[idx], submitChannel) 130 | } 131 | } 132 | } 133 | 134 | close(submitChannel) 135 | }() 136 | 137 | return submitChannel 138 | } 139 | -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | // Package server is a proof of concept to enable an HTTP interface to Tidalwave. 2 | package server 3 | 4 | import ( 5 | "io" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | "time" 10 | 11 | "github.com/busbud/tidalwave/logger" 12 | "github.com/busbud/tidalwave/parser" 13 | "github.com/labstack/echo/v4" 14 | "github.com/labstack/echo/v4/middleware" 15 | "github.com/spf13/viper" 16 | ) 17 | 18 | func jsonError(ctx echo.Context, err error) error { 19 | return ctx.JSON(500, map[string]string{"error": err.Error()}) 20 | } 21 | 22 | // New creates and starts the API server 23 | func New(version string) { 24 | logger.Log.Info("Starting Server") 25 | 26 | app := echo.New() 27 | app.HideBanner = true 28 | app.Use(middleware.Gzip()) 29 | app.Use(middleware.CORS()) 30 | app.Use(middleware.Logger()) 31 | app.Use(middleware.Recover()) 32 | 33 | app.GET("/", func(ctx echo.Context) error { 34 | return ctx.JSON(200, map[string]string{"status": "up"}) 35 | }) 36 | 37 | app.GET("/query", func(ctx echo.Context) error { 38 | queryString := ctx.QueryParam("q") 39 | logger.Log.Debug(map[string]string{"query": queryString}) 40 | if len(queryString) < 6 { 41 | // TODO Silly error. 42 | return ctx.JSON(400, map[string]string{"error": "Query length needs to be greater then 6"}) 43 | } 44 | 45 | start := time.Now() 46 | defer func() { 47 | elapsed := time.Since(start) 48 | logger.Log.Debug("Execution time: %s\n", elapsed) 49 | }() 50 | 51 | queryResults := parser.Query(queryString) 52 | 53 | switch results := queryResults.(type) { 54 | case parser.ChannelResults: 55 | r, w := io.Pipe() 56 | go ctx.Stream(200, "application/json", r) //nolint:errcheck // Don't care if there's errors. 57 | _, err := w.Write([]byte(`{"type":"` + results.Type + `","results":[`)) 58 | if err != nil { 59 | logger.Log.Debug(err) 60 | } 61 | 62 | first := true 63 | for line := range results.Channel { 64 | if first { 65 | _, err = w.Write(line) 66 | if err != nil { 67 | logger.Log.Warn(err) 68 | } 69 | 70 | first = false 71 | } else { 72 | _, err = w.Write([]byte(",")) // TODO This breaks sometimes and is missing a comma (wat?) 73 | if err != nil { 74 | logger.Log.Warn(err) 75 | } 76 | 77 | _, err = w.Write(line) 78 | if err != nil { 79 | logger.Log.Warn(err) 80 | } 81 | } 82 | } 83 | 84 | _, err = w.Write([]byte("]}")) 85 | if err != nil { 86 | logger.Log.Warn(err) 87 | } 88 | 89 | err = w.Close() 90 | if err != nil { 91 | logger.Log.Warn(err) 92 | } 93 | case parser.ArrayResults: 94 | bytes, err := results.MarshalJSON() 95 | if err != nil { 96 | return jsonError(ctx, err) 97 | } 98 | return ctx.JSONBlob(200, bytes) 99 | case parser.ObjectResults: 100 | bytes, err := results.MarshalJSON() 101 | if err != nil { 102 | return jsonError(ctx, err) 103 | } 104 | return ctx.JSONBlob(200, bytes) 105 | case parser.IntResults: 106 | bytes, err := results.MarshalJSON() 107 | if err != nil { 108 | return jsonError(ctx, err) 109 | } 110 | return ctx.JSONBlob(200, bytes) 111 | default: 112 | return ctx.JSON(400, map[string]string{"error": "Query type not supported"}) 113 | } 114 | 115 | return nil 116 | }) 117 | 118 | app.GET("/query-for-lines", func(ctx echo.Context) error { 119 | queryString := ctx.QueryParam("q") 120 | logger.Log.Debug(map[string]string{"query": queryString}) 121 | if len(queryString) < 6 { 122 | // TODO Silly error. 123 | return ctx.JSON(400, map[string]string{"error": "Query length needs to be greater then 6"}) 124 | } 125 | 126 | start := time.Now() 127 | defer func() { 128 | elapsed := time.Since(start) 129 | logger.Log.Debug("Execution time: %s\n", elapsed) 130 | }() 131 | 132 | queryResults := parser.Query(queryString) 133 | 134 | switch results := queryResults.(type) { 135 | case parser.ChannelResults: 136 | r, w := io.Pipe() 137 | go ctx.Stream(200, "application/json-seq", r) //nolint:errcheck // Don't care if there's errors. 138 | for line := range results.Channel { 139 | _, err := w.Write([]byte("\n")) 140 | if err != nil { 141 | logger.Log.Warn(err) 142 | } 143 | 144 | _, err = w.Write(line) 145 | if err != nil { 146 | logger.Log.Warn(err) 147 | } 148 | } 149 | err := w.Close() 150 | if err != nil { 151 | logger.Log.Warn(err) 152 | } 153 | case parser.ArrayResults: 154 | return ctx.JSON(400, map[string]string{"error": "Array results not supportred on /query-by-line. Use /query instead."}) 155 | case parser.ObjectResults: 156 | return ctx.JSON(400, map[string]string{"error": "Object results not supportred on /query-by-line. Use /query instead."}) 157 | case parser.IntResults: 158 | return ctx.JSON(400, map[string]string{"error": "Integer results not supportred on /query-by-line. Use /query instead."}) 159 | default: 160 | return ctx.JSON(400, map[string]string{"error": "Query type not supported"}) 161 | } 162 | 163 | return nil 164 | }) 165 | 166 | go app.Start(":" + viper.GetString("port")) //nolint:errcheck // Don't care if there's errors. 167 | 168 | c := make(chan os.Signal, 1) 169 | signal.Notify(c, os.Interrupt, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) 170 | for range c { 171 | logger.Log.Info("Exit signal received, closing...") 172 | err := app.Close() 173 | if err != nil { 174 | logger.Log.Debug(err) 175 | } 176 | os.Exit(0) 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /sqlquery/cases.go: -------------------------------------------------------------------------------- 1 | // Package sqlquery handles parsing SQL and converting to a dialect for Tidalwave. 2 | package sqlquery 3 | 4 | import ( 5 | "github.com/dustinblackman/moment" 6 | ) 7 | 8 | func stripQuotes(s string) string { 9 | if len(s) > 0 && (s[0] == '"' || s[0] == '\'') { 10 | s = s[1:] 11 | } 12 | if len(s) > 0 && (s[len(s)-1] == '"' || s[len(s)-1] == '\'') { 13 | s = s[:len(s)-1] 14 | } 15 | 16 | return s 17 | } 18 | 19 | // ProcessInt handles processing an integer in a query 20 | func ProcessInt(q *QueryParam, res int) bool { 21 | switch q.Operator { 22 | case "exists": 23 | return true 24 | case "in": 25 | for _, val := range q.ValIntArray { 26 | if val == res { 27 | return true 28 | } 29 | } 30 | return false 31 | case "=", "==": 32 | if res == q.ValInt { 33 | return true 34 | } 35 | case "!=": 36 | if res != q.ValInt { 37 | return true 38 | } 39 | case ">": 40 | if res > q.ValInt { 41 | return true 42 | } 43 | case ">=": 44 | if res >= q.ValInt { 45 | return true 46 | } 47 | case "<": 48 | if res < q.ValInt { 49 | return true 50 | } 51 | case "<=": 52 | if res <= q.ValInt { 53 | return true 54 | } 55 | } 56 | 57 | return false 58 | } 59 | 60 | // ProcessString handles processing a string for a query 61 | func ProcessString(q *QueryParam, res string) bool { 62 | switch q.Operator { // TODO: Support "like", both for value and entire log string 63 | case "exists": 64 | if len(res) > 0 { 65 | return true 66 | } 67 | case "in": 68 | for _, val := range q.ValStringArray { 69 | if val == res { 70 | return true 71 | } 72 | } 73 | return false 74 | case "like", "ilike", "~~", "~~*": 75 | return q.Regex.MatchString(res) 76 | case "=", "==": 77 | if res == q.ValString { 78 | return true 79 | } 80 | case "!=": 81 | if res != q.ValString { 82 | return true 83 | } 84 | } 85 | return false 86 | } 87 | 88 | func withinDay(logDate, dayStart, dayEnd moment.Moment) bool { 89 | return logDate.IsSame(dayStart, "YYYY-MM-DD") && logDate.IsSame(dayEnd, "YYYY-MM-DD") 90 | } 91 | 92 | // ProcessDate handles processing a date in a query 93 | func ProcessDate(d *DateParam, logDate moment.Moment, dateOnly bool) bool { 94 | if d.Date == "" { 95 | return true 96 | } 97 | 98 | dayStart := *d.DateTime.Clone().StartOfDay().SubSeconds(1) 99 | dayEnd := *d.DateTime.Clone().EndOfDay() 100 | 101 | switch d.Operator { 102 | case "exists": 103 | return true 104 | case "=", "==": 105 | if d.DateTime.IsSame(logDate, queryDateFormat) || (!d.TimeUsed && withinDay(logDate, dayStart, dayEnd)) { 106 | return true 107 | } 108 | case "!=": 109 | if !d.DateTime.IsSame(logDate, queryDateFormat) || (!d.TimeUsed && !withinDay(logDate, dayStart, dayEnd)) { 110 | return true 111 | } 112 | case ">": 113 | if dateOnly { 114 | startOfDay := d.DateTime.Clone().StartOfDay() 115 | if !d.TimeUsed && startOfDay.IsBefore(logDate) { 116 | return true 117 | } 118 | if d.TimeUsed && (startOfDay.IsBefore(logDate) || startOfDay.IsSame(logDate, "YYYY-MM-DD")) { 119 | return true 120 | } 121 | } 122 | if d.DateTime.IsBefore(logDate) { 123 | return true 124 | } 125 | case ">=": 126 | if dateOnly { 127 | startOfDay := d.DateTime.Clone().StartOfDay() 128 | if startOfDay.IsBefore(logDate) || startOfDay.IsSame(logDate, "YYYY-MM-DD") { 129 | return true 130 | } 131 | } 132 | if d.DateTime.IsBefore(logDate) || d.DateTime.IsSame(logDate, queryDateFormat) { 133 | return true 134 | } 135 | case "<": 136 | if dateOnly { 137 | startOfDay := d.DateTime.Clone().StartOfDay() 138 | if !d.TimeUsed && startOfDay.IsAfter(logDate) { 139 | return true 140 | } 141 | if d.TimeUsed && (startOfDay.IsAfter(logDate) || startOfDay.IsSame(logDate, "YYYY-MM-DD")) { 142 | return true 143 | } 144 | } 145 | if d.DateTime.IsAfter(logDate) { 146 | return true 147 | } 148 | case "<=": 149 | if dateOnly { 150 | startOfDay := d.DateTime.Clone().StartOfDay() 151 | if startOfDay.IsAfter(logDate) || startOfDay.IsSame(logDate, "YYYY-MM-DD") { 152 | return true 153 | } 154 | } 155 | if d.DateTime.IsAfter(logDate) || d.DateTime.IsSame(logDate, queryDateFormat) { 156 | return true 157 | } 158 | } 159 | return false 160 | } 161 | -------------------------------------------------------------------------------- /sqlquery/dates.go: -------------------------------------------------------------------------------- 1 | // Package sqlquery handles parsing SQL and converting to a dialect for Tidalwave. 2 | package sqlquery 3 | 4 | import ( 5 | "strings" 6 | 7 | "github.com/busbud/tidalwave/logger" 8 | "github.com/dustinblackman/moment" 9 | "github.com/jinzhu/copier" 10 | ) 11 | 12 | const queryDateFormat = "YYYY-MM-DDTHH:mm:ss" 13 | 14 | // DateParam stores date query information. 15 | type DateParam struct { 16 | Date string 17 | DateTime *moment.Moment 18 | Operator string 19 | TimeUsed bool 20 | Type string 21 | } 22 | 23 | func createDateParam(date, operator string) []DateParam { 24 | dateParam := DateParam{Operator: operator, TimeUsed: true} 25 | date = stripQuotes(date) 26 | if len(date) > 0 && len(date) <= 10 { 27 | dateParam.TimeUsed = false 28 | if operator == "<=" { 29 | date += "T23:59:59" 30 | } else { 31 | date += "T00:00:00" 32 | } 33 | } 34 | dateParam.Date = date 35 | dateParam.Type = "start" 36 | if operator == "<" || operator == "<=" { 37 | dateParam.Type = "end" 38 | } 39 | 40 | dateParam.DateTime = moment.New().Moment(queryDateFormat, date) 41 | 42 | if operator == "=" && !dateParam.TimeUsed { 43 | returnDateParam := DateParam{} 44 | err := copier.Copy(&returnDateParam, &dateParam) 45 | if err != nil { 46 | logger.Log.Fatal(err) 47 | } 48 | 49 | dateParam.Operator = ">=" 50 | returnDateParam.Operator = "<=" 51 | returnDateParam.Type = "end" 52 | 53 | endDate := strings.Split(date, "T")[0] + "T23:59:59" 54 | returnDateParam.Date = endDate 55 | returnDateParam.DateTime = moment.New().Moment(queryDateFormat, endDate) 56 | 57 | return []DateParam{dateParam, returnDateParam} 58 | } 59 | 60 | return []DateParam{dateParam} 61 | } 62 | -------------------------------------------------------------------------------- /sqlquery/query.go: -------------------------------------------------------------------------------- 1 | // Package sqlquery handles parsing SQL and converting to a dialect for Tidalwave. 2 | package sqlquery 3 | 4 | import ( 5 | "regexp" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/busbud/tidalwave/logger" 10 | "github.com/davecgh/go-spew/spew" 11 | pgQuery "github.com/lfittl/pg_query_go" 12 | pgNodes "github.com/lfittl/pg_query_go/nodes" 13 | "github.com/tidwall/gjson" 14 | dry "github.com/ungerik/go-dry" 15 | ) 16 | 17 | const ( 18 | // TypeCount specifies result is a count result 19 | TypeCount = "count" 20 | // TypeDistinct specifies result is a distinct result 21 | TypeDistinct = "distinct" 22 | // TypeCountDistinct specifies result is a count distinct result 23 | TypeCountDistinct = "count-distinct" 24 | // TypeSearch specifies specifies result is a search result 25 | TypeSearch = "search" 26 | 27 | // OperatorBetween constant. 28 | OperatorBetween = "between" 29 | // OperatorIn constant. 30 | OperatorIn = "in" 31 | ) 32 | 33 | // List of operators that use the Regex field in QueryParam 34 | var regexOperators = []string{"regexp", "~~", "~~*"} 35 | 36 | // List of supported postgres functions 37 | var supportedFunctions = []string{"count", "distinct"} 38 | 39 | // A list of strings replaced in a query string before being passed to the parser to avoid parsing errors. 40 | var stringReplacements = [][]string{ 41 | {"-", "__dash__"}, 42 | {".#.", ".__map__."}, 43 | {"''", "__twosinglequotes__"}, 44 | } 45 | 46 | // QueryParam holds a single piece of a queries WHERE and SELECT statements to be processed on log lines 47 | type QueryParam struct { 48 | IsInt bool 49 | KeyName string 50 | KeyPath string 51 | Regex *regexp.Regexp 52 | Operator string 53 | ValInt int 54 | ValIntArray []int 55 | ValString string 56 | ValStringArray []string 57 | } 58 | 59 | // QueryParams holds all the information for a given query such SELECT, FROM, and WHERE statements to be easily processed later. 60 | type QueryParams struct { 61 | SQLString string 62 | SQLStringLower string 63 | 64 | From []string // TODO: Rename to Froms 65 | 66 | AggrPath string 67 | Dates []DateParam 68 | Queries []QueryParam // TODO Rename to Where 69 | QueryKeys []string 70 | Selects []string 71 | Type string 72 | } 73 | 74 | func convertAConst(expr pgNodes.A_Const) string { 75 | switch val := expr.Val.(type) { 76 | case pgNodes.String: 77 | return val.Str 78 | case pgNodes.Integer: 79 | return strconv.Itoa(int(val.Ival)) 80 | } 81 | 82 | return "" // TODO 83 | } 84 | 85 | // Postgres' SQL parser doesn't like some characters in parts of the query. 86 | // We replace them in New, and them restore them here after parsing the sql parsers response. 87 | func (qp *QueryParams) repairString(key string) string { 88 | for _, entry := range stringReplacements { 89 | key = strings.ReplaceAll(key, entry[1], entry[0]) 90 | } 91 | 92 | // Postgres' parser makes the entire string lower case before parsing it. This restores the casing. 93 | idx := strings.Index(qp.SQLStringLower, strings.ToLower(key)) 94 | return strings.ReplaceAll(qp.SQLString[idx:idx+len(key)], "''", "'") 95 | } 96 | 97 | func (qp *QueryParams) getSelectNodeString(selectNodeVal pgNodes.ColumnRef) string { 98 | selectStrings := []string{} 99 | for _, item := range selectNodeVal.Fields.Items { 100 | switch item := item.(type) { //nolint:gocritic // You have to use switch statements for types. 101 | case pgNodes.String: 102 | selectStrings = append(selectStrings, item.Str) 103 | } 104 | } 105 | 106 | return qp.repairString(strings.Join(selectStrings, ".")) 107 | } 108 | 109 | func (qp *QueryParams) assignTypeFieldsToParam(param QueryParam, value string) QueryParam { 110 | param.ValString = qp.repairString(stripQuotes(value)) 111 | if i, err := strconv.Atoi(value); err == nil { 112 | param.IsInt = true 113 | param.ValInt = i 114 | } else if dry.StringListContains(regexOperators, param.Operator) { 115 | // Handles building the Regex field on param when a string is selected 116 | regexString := "" 117 | if param.ValString[:1] == "%" && param.ValString[len(param.ValString)-1:] != "%" { 118 | regexString = "^" + param.ValString[1:] 119 | } else if param.ValString[:1] != "%" && param.ValString[len(param.ValString)-1:] == "%" { 120 | regexString = param.ValString[:len(param.ValString)-1] + "$" 121 | } else if param.ValString[:1] == "%" && param.ValString[len(param.ValString)-1:] == "%" { 122 | regexString = param.ValString[1 : len(param.ValString)-1] 123 | } else { 124 | regexString = param.ValString 125 | } 126 | 127 | if param.Operator == "~~*" { 128 | regexString = "(?i)" + regexString 129 | } 130 | param.Regex = regexp.MustCompile(regexString) 131 | } 132 | 133 | return param 134 | } 135 | 136 | func (qp *QueryParams) handleCompareExpr(expr pgNodes.A_Expr) []QueryParam { 137 | // Param root used for everything except BETWEEN. 138 | param := QueryParam{ 139 | KeyPath: qp.getSelectNodeString(expr.Lexpr.(pgNodes.ColumnRef)), 140 | Operator: strings.ToLower(expr.Name.Items[0].(pgNodes.String).Str), 141 | } 142 | 143 | switch right := expr.Rexpr.(type) { 144 | case pgNodes.A_Const: 145 | param = qp.assignTypeFieldsToParam(param, convertAConst(right)) 146 | 147 | case pgNodes.List: 148 | if param.Operator == OperatorBetween { 149 | fromQuery := qp.assignTypeFieldsToParam(QueryParam{ 150 | KeyPath: param.KeyPath, 151 | Operator: ">=", 152 | }, convertAConst(right.Items[0].(pgNodes.A_Const))) 153 | toQuery := qp.assignTypeFieldsToParam(QueryParam{ 154 | KeyPath: param.KeyPath, 155 | Operator: "<=", 156 | }, convertAConst(right.Items[1].(pgNodes.A_Const))) 157 | 158 | return []QueryParam{fromQuery, toQuery} 159 | } 160 | 161 | // If we're comparing to a list, there's no way the operator is "=". Change it to "IN". 162 | param.Operator = OperatorIn 163 | 164 | for _, val := range right.Items { 165 | val := convertAConst(val.(pgNodes.A_Const)) 166 | if i, err := strconv.Atoi(val); err == nil { 167 | param.IsInt = true 168 | param.ValIntArray = append(param.ValIntArray, i) 169 | } else { 170 | param.ValStringArray = append(param.ValStringArray, qp.repairString(stripQuotes(val))) 171 | } 172 | } 173 | 174 | // We can't have mixed types wheh comparing arrays. We default to strings if not all values were convertable to 175 | // numbers 176 | if len(param.ValIntArray) > 0 && len(param.ValStringArray) != 0 { 177 | param.IsInt = false 178 | for _, val := range param.ValIntArray { 179 | param.ValStringArray = append(param.ValStringArray, string(val)) 180 | } 181 | param.ValIntArray = []int{} 182 | } 183 | } 184 | 185 | return []QueryParam{param} 186 | } 187 | 188 | func (qp *QueryParams) handleAndExpr(expr pgNodes.BoolExpr) []QueryParam { 189 | params := []QueryParam{} 190 | for _, whereExpr := range expr.Args.Items { 191 | params = append(params, qp.handleExpr(whereExpr)...) 192 | } 193 | 194 | return params 195 | } 196 | 197 | func (qp *QueryParams) handleExpr(entry interface{}) []QueryParam { 198 | params := []QueryParam{} 199 | switch expr := entry.(type) { 200 | case pgNodes.A_Expr: 201 | params = append(params, qp.handleCompareExpr(expr)...) 202 | case pgNodes.BoolExpr: 203 | params = append(params, qp.handleAndExpr(expr)...) 204 | } 205 | 206 | return params 207 | } 208 | 209 | // ProcessLine interates through all Queries created during the query parsing returning a bool stating whether all matched. 210 | func (qp *QueryParams) ProcessLine(line *[]byte) bool { 211 | for idx, path := range qp.QueryKeys { 212 | value := gjson.GetBytes(*line, path) 213 | if value.Type == 0 { // gjson way of saying key not found 214 | return false 215 | } 216 | 217 | q := &qp.Queries[idx] 218 | if q.IsInt && value.Type == gjson.Number { 219 | if !ProcessInt(q, int(value.Num)) { 220 | return false 221 | } 222 | } else { 223 | if !ProcessString(q, value.String()) { 224 | return false 225 | } 226 | } 227 | } 228 | 229 | return true 230 | } 231 | 232 | // New parses a query string and returns a newly created QueryParams struc holding all parsed data. 233 | func New(queryString string) *QueryParams { 234 | logger.Log.Debug("Query: " + queryString) 235 | qp := QueryParams{ 236 | SQLString: queryString, 237 | SQLStringLower: strings.ToLower(queryString), 238 | Type: TypeSearch, // Default is search. TODO Move to if statement 239 | } 240 | 241 | // Replace characters that the SQL parser won't accept that will be reverted back after parsing 242 | for _, entry := range stringReplacements { 243 | queryString = strings.ReplaceAll(queryString, entry[0], entry[1]) 244 | } 245 | 246 | tree, err := pgQuery.Parse(queryString) 247 | if err != nil { 248 | logger.Log.Error(err.Error()) 249 | } 250 | 251 | logger.Log.Debugf("Query Tree: %s", spew.Sdump(tree)) 252 | statement := tree.Statements[0].(pgNodes.RawStmt).Stmt.(pgNodes.SelectStmt) 253 | isDistrinct := len(statement.DistinctClause.Items) > 0 254 | 255 | // Where clauses 256 | if statement.WhereClause != nil { 257 | for _, entry := range qp.handleExpr(statement.WhereClause) { 258 | if entry.KeyPath == "date" { 259 | qp.Dates = append(qp.Dates, createDateParam(entry.ValString, entry.Operator)...) 260 | } else { 261 | qp.Queries = append(qp.Queries, entry) 262 | } 263 | } 264 | } 265 | 266 | // Select statements 267 | for _, selectNode := range statement.TargetList.Items { 268 | selectNode := selectNode.(pgNodes.ResTarget) 269 | keyName := "" 270 | keyPath := "" 271 | 272 | if selectNode.Name != nil { 273 | keyName = *selectNode.Name 274 | } 275 | 276 | switch selectNodeVal := selectNode.Val.(type) { 277 | case pgNodes.ColumnRef: // Regular select statement 278 | keyPath = qp.getSelectNodeString(selectNodeVal) 279 | if len(keyPath) > 0 { 280 | if keyName == "" { 281 | keySplit := strings.Split(keyPath, ".") 282 | keyName = keySplit[len(keySplit)-1] 283 | } 284 | 285 | // TODO Kill the need for SELECTS 286 | qp.Selects = append(qp.Selects, keyPath) 287 | qp.Queries = append(qp.Queries, QueryParam{ 288 | KeyName: keyName, 289 | KeyPath: keyPath, 290 | Operator: "exists", 291 | }) 292 | } 293 | 294 | case pgNodes.FuncCall: // COUNT 295 | if len(selectNodeVal.Args.Items) > 0 { 296 | qp.AggrPath = qp.getSelectNodeString(selectNodeVal.Args.Items[0].(pgNodes.ColumnRef)) 297 | qp.Selects = append(qp.Selects, qp.AggrPath) 298 | qp.Queries = append(qp.Queries, QueryParam{ 299 | KeyPath: qp.AggrPath, 300 | Operator: "exists", 301 | }) 302 | } 303 | 304 | funcType := selectNodeVal.Funcname.Items[0].(pgNodes.String).Str 305 | if !dry.StringListContains(supportedFunctions, funcType) { 306 | logger.Log.Panicf("%s is not a supported function", funcType) 307 | } 308 | 309 | // Default to just support count and distinct for now. Redo this later. 310 | if selectNodeVal.AggDistinct { 311 | qp.Type = TypeCountDistinct 312 | } else { 313 | qp.Type = TypeCount 314 | } 315 | } 316 | 317 | if isDistrinct && qp.Type != TypeCountDistinct { 318 | qp.AggrPath = keyPath 319 | qp.Type = TypeDistinct 320 | } 321 | } 322 | 323 | // From clauses 324 | for _, fromNode := range statement.FromClause.Items { 325 | qp.From = append(qp.From, qp.repairString(*fromNode.(pgNodes.RangeVar).Relname)) 326 | } 327 | 328 | // Create QueryKeys to be used by ProcessLine 329 | for _, query := range qp.Queries { 330 | qp.QueryKeys = append(qp.QueryKeys, query.KeyPath) 331 | } 332 | 333 | logger.Log.Debugf("Query Params: %s", spew.Sdump(qp)) 334 | return &qp 335 | } 336 | -------------------------------------------------------------------------------- /tidalwave.go: -------------------------------------------------------------------------------- 1 | // Package main is the entry point for Tidalwave. 2 | package main 3 | 4 | import ( 5 | "log" 6 | 7 | "github.com/busbud/tidalwave/cmd" 8 | ) 9 | 10 | func main() { 11 | if err := cmd.New().Execute(); err != nil { 12 | log.Fatal(err) 13 | } 14 | } 15 | --------------------------------------------------------------------------------