├── .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 |
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 |
--------------------------------------------------------------------------------