├── .dockerignore ├── .github └── FUNDING.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── config.yaml ├── example ├── data-gen │ ├── go.mod │ ├── main.go │ └── sample.Dockerfile ├── docker-compose.yaml ├── grafana │ ├── dashboards │ │ ├── ds_prometheus.yaml │ │ └── example.json │ └── datasources │ │ └── prometheus.yaml └── prometheus.yml ├── go.mod ├── go.sum ├── images ├── gopher.png └── grafana.png ├── main.go ├── pkg ├── config │ ├── config.go │ └── config_test.go ├── query │ ├── count.go │ ├── mongo.go │ ├── mongo_test.go │ ├── sql.go │ └── sql_test.go └── scheduler │ ├── scheduler.go │ └── scheduler_test.go └── test ├── invalid_config.yaml └── test_config.yaml /.dockerignore: -------------------------------------------------------------------------------- 1 | test/ 2 | example/ -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | custom: https://www.buymeacoffee.com/yolossn 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | data/* 2 | .env 3 | .vscode/* -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.13 2 | 3 | LABEL maintainer="nssvlr@gmail.com" 4 | 5 | WORKDIR /app 6 | 7 | # COPY and download dependencies 8 | COPY ./go.mod . 9 | RUN go mod download 10 | 11 | 12 | COPY . . 13 | 14 | RUN go build -o app 15 | 16 | CMD ["./app"] 17 | 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Santhosh Nagaraj 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

query2metric

2 |

A tool to run db queries in defined frequency and expose the count as prometheus metrics.

3 |

4 | 5 |

6 | 7 | ## Why ? 8 | 9 | Product metrics play an important role in understanding product adoption and historic metrics helps answer many questions about a product (for eg: which day of the week do I get the most signups). One common thing is that most of these metrics are extracted by querying the databases. The tool takes queries and time frequency as configuration and runs the queries in the specified intervals and exposes the output as prometheus metrics. 10 | 11 | ## Example 12 | 13 | Create a config.yaml file. 14 | 15 | > config.yaml 16 | 17 | ```yml 18 | connections: 19 | - name: mongodb1 20 | type: MONGO 21 | connectionStringFromEnv: MONGO_CONN 22 | metrics: 23 | - name: active_user_count 24 | helpString: users in the product 25 | database: test 26 | collection: test 27 | query: '{"is_active":true}' 28 | time: 10 29 | - name: total_user_count 30 | helpString: users in the product 31 | database: test 32 | collection: test 33 | query: "" 34 | time: 120 35 | - name: postgres1 36 | type: SQL 37 | connectionStringFromEnv: POSTGRES_CONN 38 | metrics: 39 | - name: template_count 40 | helpString: products in the db 41 | query: select * from templates 42 | time: 2 43 | - name: active_template_count 44 | helpString: products in the db 45 | query: error 46 | time: 4 47 | ``` 48 | 49 | Along with the metrics defined, the success and failure count of queries are also exposed as prometheus metrics. 50 | 51 | `query2metric_success_count` - No of successful queries coverted to metrics. 52 | 53 | `query2metric_error_count` - No of errors when converting query to metric. 54 | 55 | Note: Errors can occur due to invalid queries or connection issues to the db, one can use the logs to debug the issues. 56 | 57 | ## How to use ? 58 | 59 | At present the tool supports mongo and sql queries. Just create a config.yaml file and run the code. 60 | 61 | ### Mongo 62 | 63 | set `type` as `MONGO` and metrics as given in example with `query`,`time` (in seconds) etc. 64 | 65 | ```yml 66 | connections: 67 | - name: mongodb1 68 | type: MONGO 69 | connectionStringFromEnv: MONGO_CONN 70 | metrics: 71 | - name: active_user_count 72 | helpString: users in the product 73 | database: test 74 | collection: test 75 | query: '{"is_active":true}' 76 | time: 10 77 | ``` 78 | 79 | ### SQL 80 | 81 | set `type` as `SQL` and metrics as give in example. 82 | 83 | ```yml 84 | connections: 85 | - name: postgres1 86 | type: SQL 87 | connectionStringFromEnv: POSTGRES_CONN 88 | metrics: 89 | - name: template_count 90 | helpString: products in the db 91 | query: select * from templates 92 | time: 2 93 | ``` 94 | 95 | ## Run example using docker 96 | 97 | You can run the example along with prometheus and grafana using docker. 98 | 99 | docker-compose.yaml 100 | 101 | > docker-compose up 102 | 103 | metrics output: [localhost:8090/metrics](http://localhost:8090/metrics). 104 | 105 | prometheus dashboard: [localhost:9090/graph](http://localhost:9090/graph). 106 | 107 | grafana dashboard: [localhost:3000/d/qqTN2unMk/example?orgId=1](http://localhost:3000/d/qqTN2unMk/example?orgId=1). 108 | 109 | Example Output: 110 | 111 |

112 | 113 |

114 | 115 | ## Credits 116 | 117 | - Logo credit [gopherize.me](gopherize.me) 118 | -------------------------------------------------------------------------------- /config.yaml: -------------------------------------------------------------------------------- 1 | connections: 2 | - name: mongodb1 3 | type: MONGO 4 | connectionStringFromEnv: MONGO_CONN 5 | metrics: 6 | - name: active_user_count 7 | helpString: users in the product 8 | database: test 9 | collection: test 10 | query: '{"is_active":true}' 11 | time: 10 12 | - name: total_user_count 13 | helpString: users in the product 14 | database: test 15 | collection: test 16 | query: "" 17 | time: 120 18 | - name: postgres1 19 | type: SQL 20 | connectionStringFromEnv: POSTGRES_CONN 21 | metrics: 22 | - name: template_count 23 | helpString: products in the db 24 | query: select * from templates 25 | time: 2 26 | - name: active_template_count 27 | helpString: products in the db 28 | query: error 29 | time: 4 30 | -------------------------------------------------------------------------------- /example/data-gen/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/yolossn/query2metric 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/go-sql-driver/mysql v1.5.0 7 | github.com/lib/pq v1.7.0 8 | github.com/mongodb/mongo-tools-common v4.0.1+incompatible 9 | github.com/pkg/errors v0.8.1 10 | github.com/prometheus/client_golang v1.4.0 11 | github.com/prometheus/client_model v0.2.0 12 | github.com/sirupsen/logrus v1.4.2 13 | github.com/smartystreets/goconvey v1.6.4 // indirect 14 | github.com/stretchr/testify v1.4.0 15 | github.com/xo/dburl v0.0.0-20200124232849-e9ec94f52bc3 16 | go.mongodb.org/mongo-driver v1.3.5 17 | gopkg.in/yaml.v2 v2.3.0 18 | ) 19 | -------------------------------------------------------------------------------- /example/data-gen/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "os" 7 | "time" 8 | 9 | _ "github.com/lib/pq" 10 | "github.com/xo/dburl" 11 | "go.mongodb.org/mongo-driver/mongo" 12 | "go.mongodb.org/mongo-driver/mongo/options" 13 | ) 14 | 15 | // This script is to add random data to the db 16 | // to demonstrate the output/ 17 | 18 | func main() { 19 | time.Sleep(time.Duration(10) * time.Second) 20 | ctx := context.Background() 21 | // postgres connection 22 | postgresconnectionEnv := "POSTGRES_CONN" 23 | postgresconnectionStr := os.Getenv(postgresconnectionEnv) 24 | db, err := dburl.Open(postgresconnectionStr) 25 | if err != nil { 26 | log.Fatal(err) 27 | } 28 | 29 | // mongo connection 30 | mongoconnectionEnv := "MONGO_CONN" 31 | mongoconnectionString := os.Getenv(mongoconnectionEnv) 32 | mongoClient, err := mongo.NewClient(options.Client().ApplyURI(mongoconnectionString)) 33 | if err != nil { 34 | log.Fatal(err) 35 | } 36 | err = mongoClient.Connect(ctx) 37 | if err != nil { 38 | log.Fatal(err) 39 | } 40 | 41 | dropTableStmt := `DROP TABLE IF EXISTS templates` 42 | _, err = db.Exec(dropTableStmt) 43 | if err != nil { 44 | log.Fatal(err) 45 | } 46 | // create table in postgres 47 | createTableStmt := `CREATE TABLE templates(name TEXT, is_active BOOLEAN)` 48 | _, err = db.Exec(createTableStmt) 49 | if err != nil { 50 | log.Fatal(err) 51 | } 52 | 53 | // Insert rows 54 | insertStmt := `INSERT INTO templates (name, is_active) VALUES ($1, $2)` 55 | _, err = db.Exec(insertStmt, "test1", true) 56 | if err != nil { 57 | log.Fatal(err) 58 | } 59 | // Insert documents in mongo 60 | coll := mongoClient.Database("test").Collection("test") 61 | _, err = coll.InsertOne(ctx, map[string]interface{}{"name": "test", "is_active": true}) 62 | if err != nil { 63 | log.Fatal(err) 64 | } 65 | _, err = coll.InsertOne(ctx, map[string]interface{}{"name": "test1", "is_active": false}) 66 | if err != nil { 67 | log.Fatal(err) 68 | } 69 | 70 | time.Sleep(time.Duration(3) * time.Second) 71 | _, err = coll.InsertOne(ctx, map[string]interface{}{"name": "test2", "is_active": false}) 72 | if err != nil { 73 | log.Fatal(err) 74 | } 75 | _, err = db.Exec(insertStmt, "test2", true) 76 | if err != nil { 77 | log.Fatal(err) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /example/data-gen/sample.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.13 2 | 3 | LABEL maintainer="nssvlr@gmail.com" 4 | 5 | WORKDIR /app 6 | 7 | # COPY and download dependencies 8 | COPY ./go.mod . 9 | RUN go mod download 10 | 11 | 12 | COPY . . 13 | 14 | RUN go build -o app 15 | 16 | CMD ["./app"] 17 | 18 | -------------------------------------------------------------------------------- /example/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | networks: 4 | example-network: 5 | 6 | services: 7 | query2metric: 8 | build: 9 | context: .. 10 | depends_on: 11 | - mongo 12 | - postgres 13 | - sample_data 14 | ports: 15 | - 8090:8090 16 | networks: 17 | - example-network 18 | environment: 19 | MONGO_CONN: mongodb://mongo:27017/test?connect=direct 20 | POSTGRES_CONN: postgresql://postuser:postpass@postgres/test?sslmode=disable 21 | 22 | sample_data: 23 | build: 24 | context: data-gen 25 | dockerfile: sample.Dockerfile 26 | depends_on: 27 | - mongo 28 | - postgres 29 | networks: 30 | - example-network 31 | environment: 32 | MONGO_CONN: mongodb://mongo:27017/test?connect=direct 33 | POSTGRES_CONN: postgresql://postuser:postpass@postgres/test?sslmode=disable 34 | 35 | mongo: 36 | image: mongo:4.2 37 | networks: 38 | - example-network 39 | environment: 40 | MONGO_INITDB_DATABASE: test 41 | healthcheck: 42 | test: echo 'db.runCommand("ping").ok' | mongo mongo:27017/test --quiet 43 | interval: 10s 44 | timeout: 10s 45 | retries: 5 46 | start_period: 40s 47 | 48 | postgres: 49 | image: postgres 50 | networks: 51 | - example-network 52 | environment: 53 | POSTGRES_USER: postuser 54 | POSTGRES_PASSWORD: postpass 55 | POSTGRES_DB: test 56 | healthcheck: 57 | test: ["CMD-SHELL", "pg_isready -U postuser"] 58 | interval: 10s 59 | timeout: 5s 60 | retries: 5 61 | 62 | prometheus: 63 | image: prom/prometheus 64 | volumes: 65 | - ./prometheus.yml:/etc/prometheus/prometheus.yml 66 | command: 67 | - "--config.file=/etc/prometheus/prometheus.yml" 68 | ports: 69 | - 9090:9090 70 | networks: 71 | - example-network 72 | restart: always 73 | 74 | grafana: 75 | image: grafana/grafana 76 | depends_on: 77 | - prometheus 78 | ports: 79 | - 3000:3000 80 | volumes: 81 | - ./grafana/:/etc/grafana/provisioning/ 82 | networks: 83 | - example-network 84 | restart: always 85 | -------------------------------------------------------------------------------- /example/grafana/dashboards/ds_prometheus.yaml: -------------------------------------------------------------------------------- 1 | # https://github.com/ruanbekker/docker-prometheus-grafana/blob/master/grafana/provisioning/dashboards/ds_prometheus.yml 2 | apiVersion: 1 3 | 4 | providers: 5 | - name: "Prometheus" 6 | orgId: 1 7 | folder: "" 8 | type: file 9 | disableDeletion: false 10 | editable: true 11 | options: 12 | path: /etc/grafana/provisioning/dashboards 13 | -------------------------------------------------------------------------------- /example/grafana/dashboards/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "annotations": { 3 | "list": [ 4 | { 5 | "builtIn": 1, 6 | "datasource": "-- Grafana --", 7 | "enable": true, 8 | "hide": true, 9 | "iconColor": "rgba(0, 211, 255, 1)", 10 | "name": "Annotations & Alerts", 11 | "type": "dashboard" 12 | } 13 | ] 14 | }, 15 | "editable": true, 16 | "gnetId": null, 17 | "graphTooltip": 0, 18 | "id": 1, 19 | "links": [], 20 | "panels": [ 21 | { 22 | "aliasColors": {}, 23 | "bars": false, 24 | "dashLength": 10, 25 | "dashes": false, 26 | "datasource": null, 27 | "fill": 1, 28 | "fillGradient": 0, 29 | "gridPos": { 30 | "h": 9, 31 | "w": 12, 32 | "x": 0, 33 | "y": 0 34 | }, 35 | "hiddenSeries": false, 36 | "id": 4, 37 | "legend": { 38 | "avg": false, 39 | "current": false, 40 | "max": false, 41 | "min": false, 42 | "show": true, 43 | "total": false, 44 | "values": false 45 | }, 46 | "lines": true, 47 | "linewidth": 1, 48 | "nullPointMode": "null", 49 | "options": { 50 | "dataLinks": [] 51 | }, 52 | "percentage": false, 53 | "pointradius": 2, 54 | "points": false, 55 | "renderer": "flot", 56 | "seriesOverrides": [], 57 | "spaceLength": 10, 58 | "stack": false, 59 | "steppedLine": false, 60 | "targets": [ 61 | { 62 | "expr": "mongodb1_total_user_count", 63 | "interval": "", 64 | "legendFormat": "", 65 | "refId": "A" 66 | } 67 | ], 68 | "thresholds": [], 69 | "timeFrom": null, 70 | "timeRegions": [], 71 | "timeShift": null, 72 | "title": "Total user count", 73 | "tooltip": { 74 | "shared": true, 75 | "sort": 0, 76 | "value_type": "individual" 77 | }, 78 | "type": "graph", 79 | "xaxis": { 80 | "buckets": null, 81 | "mode": "time", 82 | "name": null, 83 | "show": true, 84 | "values": [] 85 | }, 86 | "yaxes": [ 87 | { 88 | "format": "short", 89 | "label": null, 90 | "logBase": 1, 91 | "max": null, 92 | "min": null, 93 | "show": true 94 | }, 95 | { 96 | "format": "short", 97 | "label": null, 98 | "logBase": 1, 99 | "max": null, 100 | "min": null, 101 | "show": true 102 | } 103 | ], 104 | "yaxis": { 105 | "align": false, 106 | "alignLevel": null 107 | } 108 | }, 109 | { 110 | "aliasColors": {}, 111 | "bars": false, 112 | "dashLength": 10, 113 | "dashes": false, 114 | "datasource": null, 115 | "description": "", 116 | "fill": 1, 117 | "fillGradient": 0, 118 | "gridPos": { 119 | "h": 9, 120 | "w": 12, 121 | "x": 12, 122 | "y": 0 123 | }, 124 | "hiddenSeries": false, 125 | "id": 2, 126 | "legend": { 127 | "avg": false, 128 | "current": false, 129 | "max": false, 130 | "min": false, 131 | "show": true, 132 | "total": false, 133 | "values": false 134 | }, 135 | "lines": true, 136 | "linewidth": 1, 137 | "nullPointMode": "null", 138 | "options": { 139 | "dataLinks": [] 140 | }, 141 | "percentage": false, 142 | "pointradius": 2, 143 | "points": false, 144 | "renderer": "flot", 145 | "seriesOverrides": [], 146 | "spaceLength": 10, 147 | "stack": false, 148 | "steppedLine": false, 149 | "targets": [ 150 | { 151 | "expr": "postgres1_template_count", 152 | "interval": "", 153 | "legendFormat": "", 154 | "refId": "A" 155 | } 156 | ], 157 | "thresholds": [], 158 | "timeFrom": null, 159 | "timeRegions": [], 160 | "timeShift": null, 161 | "title": "Active Template Count", 162 | "tooltip": { 163 | "shared": true, 164 | "sort": 0, 165 | "value_type": "individual" 166 | }, 167 | "transparent": true, 168 | "type": "graph", 169 | "xaxis": { 170 | "buckets": null, 171 | "mode": "time", 172 | "name": null, 173 | "show": true, 174 | "values": [] 175 | }, 176 | "yaxes": [ 177 | { 178 | "format": "short", 179 | "label": null, 180 | "logBase": 1, 181 | "max": null, 182 | "min": null, 183 | "show": true 184 | }, 185 | { 186 | "format": "short", 187 | "label": null, 188 | "logBase": 1, 189 | "max": null, 190 | "min": null, 191 | "show": true 192 | } 193 | ], 194 | "yaxis": { 195 | "align": false, 196 | "alignLevel": null 197 | } 198 | }, 199 | { 200 | "cacheTimeout": null, 201 | "datasource": null, 202 | "gridPos": { 203 | "h": 8, 204 | "w": 12, 205 | "x": 0, 206 | "y": 9 207 | }, 208 | "id": 6, 209 | "links": [], 210 | "options": { 211 | "colorMode": "value", 212 | "fieldOptions": { 213 | "calcs": ["mean"], 214 | "defaults": { 215 | "mappings": [ 216 | { 217 | "id": 0, 218 | "op": "=", 219 | "text": "N/A", 220 | "type": 1, 221 | "value": "null" 222 | } 223 | ], 224 | "nullValueMode": "connected", 225 | "thresholds": { 226 | "mode": "absolute", 227 | "steps": [ 228 | { 229 | "color": "green", 230 | "value": null 231 | } 232 | ] 233 | }, 234 | "unit": "none" 235 | }, 236 | "overrides": [], 237 | "values": false 238 | }, 239 | "graphMode": "area", 240 | "justifyMode": "auto", 241 | "orientation": "horizontal" 242 | }, 243 | "pluginVersion": "6.7.3", 244 | "targets": [ 245 | { 246 | "expr": "query2metric_success_count", 247 | "instant": true, 248 | "interval": "", 249 | "legendFormat": "", 250 | "refId": "A" 251 | } 252 | ], 253 | "timeFrom": null, 254 | "timeShift": null, 255 | "title": "successful queries to metrics", 256 | "type": "stat" 257 | }, 258 | { 259 | "datasource": null, 260 | "gridPos": { 261 | "h": 8, 262 | "w": 12, 263 | "x": 12, 264 | "y": 9 265 | }, 266 | "id": 8, 267 | "options": { 268 | "colorMode": "value", 269 | "fieldOptions": { 270 | "calcs": ["mean"], 271 | "defaults": { 272 | "mappings": [], 273 | "thresholds": { 274 | "mode": "absolute", 275 | "steps": [ 276 | { 277 | "color": "green", 278 | "value": null 279 | }, 280 | { 281 | "color": "red", 282 | "value": 80 283 | } 284 | ] 285 | } 286 | }, 287 | "overrides": [], 288 | "values": false 289 | }, 290 | "graphMode": "area", 291 | "justifyMode": "auto", 292 | "orientation": "auto" 293 | }, 294 | "pluginVersion": "6.7.3", 295 | "targets": [ 296 | { 297 | "expr": "query2metric_error_count", 298 | "instant": true, 299 | "interval": "", 300 | "legendFormat": "", 301 | "refId": "A" 302 | } 303 | ], 304 | "timeFrom": null, 305 | "timeShift": null, 306 | "title": "Errors in query to metric", 307 | "type": "stat" 308 | } 309 | ], 310 | "schemaVersion": 22, 311 | "style": "dark", 312 | "tags": [], 313 | "templating": { 314 | "list": [] 315 | }, 316 | "time": { 317 | "from": "now-30m", 318 | "to": "now" 319 | }, 320 | "timepicker": {}, 321 | "timezone": "", 322 | "title": "Example", 323 | "uid": "qqTN2unMk", 324 | "variables": { 325 | "list": [] 326 | }, 327 | "version": 4 328 | } 329 | -------------------------------------------------------------------------------- /example/grafana/datasources/prometheus.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | datasources: 3 | - name: Prometheus 4 | type: prometheus 5 | access: proxy 6 | orgId: 1 7 | url: http://prometheus:9090 8 | password: 9 | user: 10 | database: 11 | basicAuth: false 12 | basicAuthUser: 13 | basicAuthPassword: 14 | withCredentials: 15 | isDefault: true 16 | jsonData: 17 | graphiteVersion: "1.1" 18 | tlsAuth: false 19 | tlsAuthWithCACert: false 20 | secureJsonData: 21 | tlsCACert: "..." 22 | tlsClientCert: "..." 23 | tlsClientKey: "..." 24 | version: 1 25 | editable: true 26 | -------------------------------------------------------------------------------- /example/prometheus.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 15s 3 | 4 | scrape_configs: 5 | - job_name: app_metrics 6 | static_configs: 7 | - targets: ["query2metric:8090"] 8 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/yolossn/query2metric 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/go-sql-driver/mysql v1.5.0 7 | github.com/lib/pq v1.7.0 8 | github.com/mongodb/mongo-tools-common v4.0.1+incompatible 9 | github.com/pkg/errors v0.8.1 10 | github.com/prometheus/client_golang v1.4.0 11 | github.com/prometheus/client_model v0.2.0 12 | github.com/sirupsen/logrus v1.4.2 13 | github.com/smartystreets/goconvey v1.6.4 // indirect 14 | github.com/stretchr/testify v1.4.0 15 | github.com/xo/dburl v0.0.0-20200124232849-e9ec94f52bc3 16 | go.mongodb.org/mongo-driver v1.3.5 17 | gopkg.in/yaml.v2 v2.3.0 18 | ) 19 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 3 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 4 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 5 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 6 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 7 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 8 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 9 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 10 | github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= 11 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 12 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 14 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 16 | github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 17 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 18 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 19 | github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= 20 | github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 21 | github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= 22 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 23 | github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0= 24 | github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY= 25 | github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg= 26 | github.com/gobuffalo/envy v1.6.15/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= 27 | github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= 28 | github.com/gobuffalo/flect v0.1.0/go.mod h1:d2ehjJqGOH/Kjqcoz+F7jHTBbmDb38yXA598Hb50EGs= 29 | github.com/gobuffalo/flect v0.1.1/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= 30 | github.com/gobuffalo/flect v0.1.3/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= 31 | github.com/gobuffalo/genny v0.0.0-20190329151137-27723ad26ef9/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk= 32 | github.com/gobuffalo/genny v0.0.0-20190403191548-3ca520ef0d9e/go.mod h1:80lIj3kVJWwOrXWWMRzzdhW3DsrdjILVil/SFKBzF28= 33 | github.com/gobuffalo/genny v0.1.0/go.mod h1:XidbUqzak3lHdS//TPu2OgiFB+51Ur5f7CSnXZ/JDvo= 34 | github.com/gobuffalo/genny v0.1.1/go.mod h1:5TExbEyY48pfunL4QSXxlDOmdsD44RRq4mVZ0Ex28Xk= 35 | github.com/gobuffalo/gitgen v0.0.0-20190315122116-cc086187d211/go.mod h1:vEHJk/E9DmhejeLeNt7UVvlSGv3ziL+djtTr3yyzcOw= 36 | github.com/gobuffalo/gogen v0.0.0-20190315121717-8f38393713f5/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360= 37 | github.com/gobuffalo/gogen v0.1.0/go.mod h1:8NTelM5qd8RZ15VjQTFkAW6qOMx5wBbW4dSCS3BY8gg= 38 | github.com/gobuffalo/gogen v0.1.1/go.mod h1:y8iBtmHmGc4qa3urIyo1shvOD8JftTtfcKi+71xfDNE= 39 | github.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2/go.mod h1:QdxcLw541hSGtBnhUc4gaNIXRjiDppFGaDqzbrBd3v8= 40 | github.com/gobuffalo/mapi v1.0.1/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= 41 | github.com/gobuffalo/mapi v1.0.2/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= 42 | github.com/gobuffalo/packd v0.0.0-20190315124812-a385830c7fc0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= 43 | github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= 44 | github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ= 45 | github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0= 46 | github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw= 47 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 48 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 49 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 50 | github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= 51 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 52 | github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= 53 | github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 54 | github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= 55 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 56 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 57 | github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= 58 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 59 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 60 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= 61 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 62 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 63 | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= 64 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 65 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 66 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 67 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 68 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 69 | github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4= 70 | github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= 71 | github.com/klauspost/compress v1.9.5 h1:U+CaK85mrNNb4k8BNOfgJtJ/gr6kswUCFj6miSzVC6M= 72 | github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= 73 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 74 | github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= 75 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 76 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 77 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 78 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 79 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 80 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 81 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 82 | github.com/lib/pq v1.7.0 h1:h93mCPfUSkaul3Ka/VG8uZdmW1uMHDGxzu0NWHuJmHY= 83 | github.com/lib/pq v1.7.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 84 | github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= 85 | github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= 86 | github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= 87 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 88 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 89 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 90 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 91 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 92 | github.com/mongodb/mongo-tools-common v4.0.1+incompatible h1:xSo5bgWUzIpQ4pMdXXzX4JYQtoQIuq54cwI/gMb1sMs= 93 | github.com/mongodb/mongo-tools-common v4.0.1+incompatible/go.mod h1:T0AGIWI5PvxSzqt5zsR5zlcHl52KGeAzhoAnSvvpEtU= 94 | github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= 95 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 96 | github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo= 97 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 98 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 99 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 100 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 101 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 102 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 103 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 104 | github.com/prometheus/client_golang v1.4.0 h1:YVIb/fVcOTMSqtqZWSKnHpSLBxu8DKgxq8z6RuBZwqI= 105 | github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= 106 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 107 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 108 | github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= 109 | github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 110 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 111 | github.com/prometheus/common v0.9.1 h1:KOMtN28tlbam3/7ZKEYKHhKoJZYYj3gMH4uc62x7X7U= 112 | github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= 113 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 114 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 115 | github.com/prometheus/procfs v0.0.8 h1:+fpWZdT24pJBiqJdAwYBjPSk+5YmQzYNPYzQsdzLkt8= 116 | github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= 117 | github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 118 | github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 119 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 120 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 121 | github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 122 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 123 | github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= 124 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 125 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= 126 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 127 | github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= 128 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 129 | github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= 130 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 131 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 132 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 133 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 134 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 135 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 136 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 137 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 138 | github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= 139 | github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= 140 | github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c h1:u40Z8hqBAAQyv+vATcGgV0YCnDjqSL7/q/JyPhhJSPk= 141 | github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= 142 | github.com/xdg/stringprep v0.0.0-20180714160509-73f8eece6fdc h1:n+nNi93yXLkJvKwXNP9d55HC7lGK4H/SRcwB5IaUZLo= 143 | github.com/xdg/stringprep v0.0.0-20180714160509-73f8eece6fdc/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= 144 | github.com/xo/dburl v0.0.0-20200124232849-e9ec94f52bc3 h1:NC3CI7do3KHtiuYhk1CdS9V2qS3jNa7Fs2Afcnnt+IE= 145 | github.com/xo/dburl v0.0.0-20200124232849-e9ec94f52bc3/go.mod h1:A47W3pdWONaZmXuLZgfKLAVgUY0qvfTRM5vVDKS40S4= 146 | go.mongodb.org/mongo-driver v1.3.5 h1:S0ZOruh4YGHjD7JoN7mIsTrNjnQbOjrmgrx6l6pZN7I= 147 | go.mongodb.org/mongo-driver v1.3.5/go.mod h1:Ual6Gkco7ZGQw8wE1t4tLnvBsf6yVSM60qW6TgOeJ5c= 148 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 149 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 150 | golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= 151 | golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5 h1:8dUaAV7K4uHsF56JQWkprecIQKdPHtR9jCHF5nB8uzc= 152 | golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 153 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 154 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 155 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 156 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 157 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 158 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 159 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 160 | golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 161 | golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= 162 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 163 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= 164 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 165 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 166 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 167 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 168 | golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 169 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 170 | golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 171 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 172 | golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 173 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82 h1:ywK/j/KkyTHcdyYSZNXGjMwgmDSfjglYZ3vStQ/gSCU= 174 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 175 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 176 | golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= 177 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 178 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 179 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 180 | golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 181 | golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 182 | golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 183 | golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 184 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 185 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 186 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 187 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 188 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 189 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 190 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 191 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 192 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 193 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 194 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 195 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 196 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 197 | gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 198 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= 199 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 200 | -------------------------------------------------------------------------------- /images/gopher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yolossn/query2metric/29118351f26affcddb7085625d7c8a98c0de536b/images/gopher.png -------------------------------------------------------------------------------- /images/grafana.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yolossn/query2metric/29118351f26affcddb7085625d7c8a98c0de536b/images/grafana.png -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/prometheus/client_golang/prometheus/promhttp" 7 | log "github.com/sirupsen/logrus" 8 | "github.com/yolossn/query2metric/pkg/config" 9 | "github.com/yolossn/query2metric/pkg/scheduler" 10 | ) 11 | 12 | func main() { 13 | conf, err := config.FromFile("config.yaml") 14 | if err != nil { 15 | log.Fatal("Error reading config file,err:", err) 16 | } 17 | 18 | configRunner := scheduler.FromConfig(*conf) 19 | err = configRunner.Start() 20 | if err != nil { 21 | log.Fatal("Error in runner,err:", err) 22 | } 23 | 24 | http.Handle("/metrics", promhttp.Handler()) 25 | log.Fatal(http.ListenAndServe(":8090", nil)) 26 | 27 | } 28 | -------------------------------------------------------------------------------- /pkg/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "io/ioutil" 5 | 6 | "github.com/pkg/errors" 7 | "gopkg.in/yaml.v2" 8 | ) 9 | 10 | const MONGO = "MONGO" 11 | const SQL = "SQL" 12 | 13 | type Metric struct { 14 | Name string `yaml:"name"` 15 | HelpString string `yaml:"helpString"` 16 | Database string `yaml:"database"` 17 | Collection string `yaml:"collection"` 18 | Query string `yaml:"query"` 19 | Time int64 `yaml:"time"` 20 | } 21 | 22 | type Connection struct { 23 | Name string `yaml:"name"` 24 | Type string `yaml:"type"` 25 | Subtype string `yaml:"subtype"` 26 | ConnectionString string `yaml:"connectionStringFromEnv"` 27 | Metrics []Metric `yaml:"metrics"` 28 | } 29 | 30 | type Config struct { 31 | Connections []Connection `yaml:"connections"` 32 | } 33 | 34 | func FromFile(file string) (*Config, error) { 35 | content, err := ioutil.ReadFile(file) 36 | if err != nil { 37 | return nil, errors.Wrap(err, "Error reading file") 38 | } 39 | 40 | conf := Config{} 41 | err = yaml.Unmarshal(content, &conf) 42 | if err != nil { 43 | return nil, errors.Wrap(err, "Error unmarshalling config") 44 | } 45 | return &conf, nil 46 | } 47 | -------------------------------------------------------------------------------- /pkg/config/config_test.go: -------------------------------------------------------------------------------- 1 | package config_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/yolossn/query2metric/pkg/config" 8 | ) 9 | 10 | func TestFromFile(t *testing.T) { 11 | t.Parallel() 12 | 13 | // File exists 14 | conf, err := config.FromFile("../../test/test_config.yaml") 15 | require.NoError(t, err) 16 | 17 | require.Equal(t, len(conf.Connections), 2) 18 | 19 | conf, err = config.FromFile("../../test/not_exists.yaml") 20 | require.Error(t, err) 21 | require.Nil(t, conf) 22 | 23 | conf, err = config.FromFile("../../test/invalid_config.yaml") 24 | require.Error(t, err) 25 | require.Nil(t, conf) 26 | } 27 | -------------------------------------------------------------------------------- /pkg/query/count.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | "github.com/yolossn/query2metric/pkg/config" 6 | ) 7 | 8 | type CountQuery interface { 9 | Count(metric config.Metric) (int64, error) 10 | } 11 | 12 | var ENV_NOT_SET = errors.New("connnectionString is empty") 13 | -------------------------------------------------------------------------------- /pkg/query/mongo.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "os" 7 | 8 | "github.com/mongodb/mongo-tools-common/bsonutil" 9 | "github.com/pkg/errors" 10 | "github.com/yolossn/query2metric/pkg/config" 11 | "go.mongodb.org/mongo-driver/mongo" 12 | "go.mongodb.org/mongo-driver/mongo/options" 13 | ) 14 | 15 | type mongoQuery struct { 16 | connection string 17 | client *mongo.Client 18 | } 19 | 20 | func NewMongoConn(connnectionURL string) (CountQuery, error) { 21 | connnectionString := os.Getenv(connnectionURL) 22 | if connnectionString == "" { 23 | return nil, ENV_NOT_SET 24 | } 25 | 26 | ctx := context.Background() 27 | 28 | mongoClient, err := mongo.NewClient(options.Client().ApplyURI(connnectionString)) 29 | if err != nil { 30 | return nil, errors.Wrap(err, "Connection error") 31 | } 32 | 33 | err = mongoClient.Connect(ctx) 34 | if err != nil { 35 | return nil, errors.Wrap(err, "Connection error") 36 | } 37 | 38 | return &mongoQuery{connnectionURL, mongoClient}, err 39 | } 40 | 41 | func (m *mongoQuery) Count(metric config.Metric) (int64, error) { 42 | 43 | ctx := context.Background() 44 | query := map[string]interface{}{} 45 | if metric.Query != "" { 46 | err := json.Unmarshal([]byte(metric.Query), &query) 47 | if err != nil { 48 | return 0, err 49 | } 50 | } 51 | bsonQuery, err := bsonutil.ConvertLegacyExtJSONValueToBSON(query) 52 | if err != nil { 53 | return 0, err 54 | } 55 | count, err := m.client.Database(metric.Database).Collection(metric.Collection).CountDocuments(ctx, bsonQuery) 56 | if err != nil { 57 | return 0, err 58 | } 59 | return count, nil 60 | } 61 | -------------------------------------------------------------------------------- /pkg/query/mongo_test.go: -------------------------------------------------------------------------------- 1 | package query_test 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | "github.com/yolossn/query2metric/pkg/config" 10 | "github.com/yolossn/query2metric/pkg/query" 11 | "go.mongodb.org/mongo-driver/mongo" 12 | "go.mongodb.org/mongo-driver/mongo/options" 13 | ) 14 | 15 | func TestNewMongoConn(t *testing.T) { 16 | t.Parallel() 17 | 18 | // Env check 19 | connectionEnv := "TEST_INVALID_MONGO_CONN" 20 | 21 | conn, err := query.NewMongoConn(connectionEnv) 22 | require.Error(t, err) 23 | require.Equal(t, err, query.ENV_NOT_SET) 24 | require.Nil(t, conn) 25 | 26 | // invalid URI check 27 | connectionStr := "mongo://test@123:27017/not_exists" 28 | 29 | conn, err = newMongoConnectionHelper(t, connectionEnv, connectionStr) 30 | require.Error(t, err) 31 | require.Nil(t, conn) 32 | 33 | // valid connect check 34 | connectionStr = "mongodb://test@123:27017/not_exists?directConnection=blah" 35 | 36 | conn, err = newMongoConnectionHelper(t, connectionEnv, connectionStr) 37 | require.NoError(t, err) 38 | require.NotNil(t, conn) 39 | 40 | } 41 | 42 | func TestMongoCount(t *testing.T) { 43 | 44 | ctx := context.Background() 45 | connectionEnv := "TEST_MONGO_CONN" 46 | connectionString := os.Getenv(connectionEnv) 47 | 48 | // Setup 49 | 50 | mongoClient, err := mongo.NewClient(options.Client().ApplyURI(connectionString)) 51 | require.NotNil(t, mongoClient) 52 | require.NoError(t, err) 53 | 54 | err = mongoClient.Connect(ctx) 55 | require.NoError(t, err) 56 | 57 | coll := mongoClient.Database("yolo_db").Collection("test") 58 | 59 | // Clean 60 | err = coll.Drop(ctx) 61 | require.NoError(t, err) 62 | 63 | // Test Data 64 | _, err = coll.InsertOne(ctx, map[string]interface{}{"name": "test", "is_active": true}) 65 | require.NoError(t, err) 66 | _, err = coll.InsertOne(ctx, map[string]interface{}{"name": "test1", "is_active": false}) 67 | require.NoError(t, err) 68 | 69 | // New Conn 70 | conn, err := newMongoConnectionHelper(t, connectionEnv, connectionString) 71 | require.NoError(t, err) 72 | require.NotNil(t, conn) 73 | 74 | // Test 75 | // Positive test cases 76 | testQuery := config.Metric{ 77 | Database: "yolo_db", 78 | Collection: "test", 79 | Query: `{"is_active":true}`, 80 | } 81 | out, err := conn.Count(testQuery) 82 | require.Equal(t, int64(1), out) 83 | require.NoError(t, err) 84 | 85 | testQuery.Query = "" 86 | out, err = conn.Count(testQuery) 87 | require.Equal(t, int64(2), out) 88 | require.NoError(t, err) 89 | 90 | // negative test cases 91 | testQuery.Query = "{test:'invalidjson'" 92 | out, err = conn.Count(testQuery) 93 | require.Equal(t, int64(0), out) 94 | require.Error(t, err) 95 | 96 | testQuery.Query = `{"test":{"$count":"{"}}` 97 | out, err = conn.Count(testQuery) 98 | require.Equal(t, int64(0), out) 99 | require.Error(t, err) 100 | 101 | testQuery.Query = `{"test":{"$count":"{"}}` 102 | out, err = conn.Count(testQuery) 103 | require.Equal(t, int64(0), out) 104 | require.Error(t, err) 105 | 106 | } 107 | 108 | func newMongoConnectionHelper(t *testing.T, connectionEnv, connectionString string) (query.CountQuery, error) { 109 | t.Helper() 110 | 111 | err := os.Setenv(connectionEnv, connectionString) 112 | require.NoError(t, err) 113 | 114 | return query.NewMongoConn(connectionEnv) 115 | } 116 | -------------------------------------------------------------------------------- /pkg/query/sql.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "os" 7 | 8 | _ "github.com/go-sql-driver/mysql" 9 | _ "github.com/lib/pq" 10 | "github.com/pkg/errors" 11 | "github.com/xo/dburl" 12 | "github.com/yolossn/query2metric/pkg/config" 13 | ) 14 | 15 | type sqlQuery struct { 16 | connection string 17 | db *sql.DB 18 | } 19 | 20 | func NewSQLQuery(connnectionURL string) (CountQuery, error) { 21 | connnectionString := os.Getenv(connnectionURL) 22 | if connnectionString == "" { 23 | return nil, ENV_NOT_SET 24 | } 25 | db, err := dburl.Open(connnectionString) 26 | if err != nil { 27 | return nil, errors.Wrap(err, "Error in establishing connection to db") 28 | } 29 | 30 | return &sqlQuery{connnectionURL, db}, nil 31 | } 32 | 33 | func (s *sqlQuery) Count(metric config.Metric) (int64, error) { 34 | if metric.Query == "" { 35 | return 0, errors.New("Query is empty") 36 | } 37 | 38 | countQuery := fmt.Sprintf("select count(*) from (%s) as count_query", metric.Query) 39 | row, err := s.db.Query(countQuery) 40 | if err != nil { 41 | return 0, errors.Wrap(err, "Error running query") 42 | } 43 | defer row.Close() 44 | var out int64 45 | if row.Next() { 46 | err := row.Scan(&out) 47 | if err != nil { 48 | return 0, errors.Wrap(err, "Error running query") 49 | } 50 | } 51 | return out, nil 52 | } 53 | -------------------------------------------------------------------------------- /pkg/query/sql_test.go: -------------------------------------------------------------------------------- 1 | package query_test 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | "github.com/xo/dburl" 9 | "github.com/yolossn/query2metric/pkg/config" 10 | "github.com/yolossn/query2metric/pkg/query" 11 | ) 12 | 13 | func TestNewSQLConn(t *testing.T) { 14 | t.Parallel() 15 | 16 | // Env check 17 | connectionEnv := "TEST_INVALID_SQL_CONN" 18 | 19 | conn, err := query.NewSQLQuery(connectionEnv) 20 | require.Error(t, err) 21 | require.Equal(t, err, query.ENV_NOT_SET) 22 | require.Nil(t, conn) 23 | 24 | // Invalid URI 25 | connectionStr := "postg://127.0.0.1/sdffsdf" 26 | conn, err = newSQLConnectionHelper(t, connectionEnv, connectionStr) 27 | require.Error(t, err) 28 | require.Nil(t, conn) 29 | 30 | // Valid URI 31 | connectionStr = os.Getenv("TEST_SQL_CONN") 32 | conn, err = newSQLConnectionHelper(t, connectionEnv, connectionStr) 33 | require.NoError(t, err) 34 | require.NotNil(t, conn) 35 | 36 | } 37 | 38 | func TestSQLCount(t *testing.T) { 39 | 40 | // Setup 41 | connectionEnv := "TEST_SQL_CONN" 42 | connectionStr := os.Getenv(connectionEnv) 43 | db, err := dburl.Open(connectionStr) 44 | require.NotNil(t, db) 45 | require.NoError(t, err) 46 | 47 | // Clean 48 | dropTableStmt := `DROP TABLE IF EXISTS test` 49 | res, err := db.Exec(dropTableStmt) 50 | require.NoError(t, err) 51 | require.NotNil(t, res) 52 | 53 | // Test Data 54 | createTableStmt := `CREATE TABLE test(name TEXT, is_active BOOLEAN)` 55 | _, err = db.Exec(createTableStmt) 56 | require.NoError(t, err) 57 | 58 | insertStmt := `INSERT INTO test (name, is_active) VALUES ($1, $2)` 59 | _, err = db.Exec(insertStmt, "test1", true) 60 | require.NoError(t, err) 61 | _, err = db.Exec(insertStmt, "test2", false) 62 | require.NoError(t, err) 63 | 64 | // New Conn 65 | conn, err := newSQLConnectionHelper(t, connectionEnv, connectionStr) 66 | require.NoError(t, err) 67 | require.NotNil(t, conn) 68 | 69 | // Test 70 | // Positive test cases 71 | testQuery := config.Metric{ 72 | Query: `select * from test where is_active = true`, 73 | } 74 | out, err := conn.Count(testQuery) 75 | require.Equal(t, int64(1), out) 76 | require.NoError(t, err) 77 | 78 | testQuery.Query = `select * from test` 79 | out, err = conn.Count(testQuery) 80 | require.Equal(t, int64(2), out) 81 | require.NoError(t, err) 82 | 83 | testQuery.Query = "" 84 | out, err = conn.Count(testQuery) 85 | require.Error(t, err) 86 | require.Equal(t, int64(0), out) 87 | 88 | // Negative test cases 89 | testQuery.Query = `test` 90 | out, err = conn.Count(testQuery) 91 | require.Error(t, err) 92 | require.Equal(t, int64(0), out) 93 | 94 | } 95 | 96 | func newSQLConnectionHelper(t *testing.T, connectionEnv, connectionString string) (query.CountQuery, error) { 97 | t.Helper() 98 | 99 | err := os.Setenv(connectionEnv, connectionString) 100 | require.NoError(t, err) 101 | 102 | return query.NewSQLQuery(connectionEnv) 103 | } 104 | -------------------------------------------------------------------------------- /pkg/scheduler/scheduler.go: -------------------------------------------------------------------------------- 1 | package scheduler 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/pkg/errors" 7 | "github.com/prometheus/client_golang/prometheus" 8 | log "github.com/sirupsen/logrus" 9 | "github.com/yolossn/query2metric/pkg/config" 10 | "github.com/yolossn/query2metric/pkg/query" 11 | ) 12 | 13 | func init() { 14 | log.SetFormatter(&log.JSONFormatter{}) 15 | log.SetLevel(log.DebugLevel) 16 | } 17 | 18 | type Scheduler struct { 19 | conf config.Config 20 | } 21 | 22 | func (s Scheduler) Start() error { 23 | errorChan := make(chan bool, 1) 24 | successChan := make(chan bool, 1) 25 | for _, conn := range s.conf.Connections { 26 | var dbConnection query.CountQuery 27 | var err error 28 | switch conn.Type { 29 | case config.MONGO: 30 | dbConnection, err = query.NewMongoConn(conn.ConnectionString) 31 | if err != nil { 32 | return err 33 | } 34 | case config.SQL: 35 | dbConnection, err = query.NewSQLQuery(conn.ConnectionString) 36 | if err != nil { 37 | return err 38 | } 39 | default: 40 | continue 41 | } 42 | 43 | for _, metric := range conn.Metrics { 44 | 45 | gaugeMetric := prometheus.NewGauge( 46 | prometheus.GaugeOpts{ 47 | Namespace: conn.Name, 48 | Name: metric.Name, 49 | Help: metric.HelpString, 50 | }, 51 | ) 52 | err = prometheus.Register(gaugeMetric) 53 | if err != nil { 54 | return errors.Wrap(err, "Error registering metric") 55 | } 56 | ticker := time.NewTicker(time.Duration(metric.Time) * time.Second) 57 | run(ticker, gaugeMetric, dbConnection, metric, successChan, errorChan) 58 | } 59 | } 60 | 61 | go errorCounter(errorChan) 62 | go successCounter(successChan) 63 | 64 | return nil 65 | } 66 | 67 | func run(tick *time.Ticker, gauge prometheus.Gauge, quer query.CountQuery, metric config.Metric, successChan, errorChan chan bool) { 68 | 69 | go func() { 70 | for { 71 | select { 72 | case <-tick.C: 73 | out, err := quer.Count(metric) 74 | if err != nil { 75 | errorChan <- true 76 | log.WithFields(log.Fields{"db": metric.Database, "metric": metric.Name, "query": metric.Query}).Error(err) 77 | } else { 78 | gauge.Set(float64(out)) 79 | successChan <- true 80 | log.WithFields(log.Fields{"db": metric.Database, "metric": metric.Name, "query": metric.Query}).Debug("success") 81 | } 82 | } 83 | } 84 | }() 85 | } 86 | 87 | func FromConfig(conf config.Config) Scheduler { 88 | return Scheduler{conf} 89 | } 90 | 91 | func errorCounter(errorChan chan bool) { 92 | 93 | errorCounter := prometheus.NewCounter( 94 | prometheus.CounterOpts{ 95 | Namespace: "query2metric", 96 | Name: "error_count", 97 | Help: "No of errors when converting query to metric", 98 | }, 99 | ) 100 | 101 | prometheus.MustRegister(errorCounter) 102 | for { 103 | switch { 104 | case <-errorChan: 105 | errorCounter.Inc() 106 | } 107 | } 108 | 109 | } 110 | 111 | func successCounter(successChan chan bool) { 112 | 113 | successCounter := prometheus.NewCounter( 114 | prometheus.CounterOpts{ 115 | Namespace: "query2metric", 116 | Name: "success_count", 117 | Help: "No of successful queries coverted to metrics", 118 | }, 119 | ) 120 | 121 | prometheus.MustRegister(successCounter) 122 | 123 | for { 124 | switch { 125 | case <-successChan: 126 | successCounter.Inc() 127 | } 128 | } 129 | 130 | } 131 | -------------------------------------------------------------------------------- /pkg/scheduler/scheduler_test.go: -------------------------------------------------------------------------------- 1 | package scheduler_test 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | "time" 7 | 8 | "github.com/prometheus/client_golang/prometheus" 9 | dto "github.com/prometheus/client_model/go" 10 | "github.com/stretchr/testify/require" 11 | "github.com/xo/dburl" 12 | "github.com/yolossn/query2metric/pkg/config" 13 | "github.com/yolossn/query2metric/pkg/scheduler" 14 | ) 15 | 16 | func TestFromConfig(t *testing.T) { 17 | t.Parallel() 18 | config := config.Config{ 19 | Connections: []config.Connection{ 20 | { 21 | Name: "test2", 22 | }, 23 | }, 24 | } 25 | sh := scheduler.FromConfig(config) 26 | require.NotNil(t, sh) 27 | } 28 | 29 | func TestSchedulerStart(t *testing.T) { 30 | t.Parallel() 31 | 32 | conf, err := config.FromFile("../../test/test_config.yaml") 33 | require.NoError(t, err) 34 | sch := scheduler.FromConfig(*conf) 35 | sch.Start() 36 | 37 | // Setup 38 | connectionEnv := "TEST_SQL_CONN" 39 | connectionStr := os.Getenv(connectionEnv) 40 | db, err := dburl.Open(connectionStr) 41 | require.NotNil(t, db) 42 | require.NoError(t, err) 43 | 44 | dropTableStmt := `DROP TABLE IF EXISTS queries` 45 | res, err := db.Exec(dropTableStmt) 46 | require.NoError(t, err) 47 | require.NotNil(t, res) 48 | 49 | // Test Data 50 | createTableStmt := `CREATE TABLE queries(name TEXT, is_active BOOLEAN)` 51 | _, err = db.Exec(createTableStmt) 52 | require.NoError(t, err) 53 | 54 | insertStmt := `INSERT INTO queries (name, is_active) VALUES ($1, $2)` 55 | _, err = db.Exec(insertStmt, "test1", true) 56 | require.NoError(t, err) 57 | _, err = db.Exec(insertStmt, "test2", false) 58 | require.NoError(t, err) 59 | 60 | // Test 61 | time.Sleep(time.Duration(5) * time.Second) 62 | out, err := prometheus.DefaultGatherer.Gather() 63 | require.NotNil(t, out) 64 | require.NoError(t, err) 65 | 66 | metric := filterMetrics(t, out, []string{"postgres1_template_count"}) 67 | require.NotNil(t, metric) 68 | require.Equal(t, 1, len(metric)) 69 | require.NotZero(t, metric[0].Metric[0].Gauge.Value) 70 | 71 | currentValue := metric[0].Metric[0].Gauge.Value 72 | // add more data 73 | _, err = db.Exec(insertStmt, "test3", false) 74 | require.NoError(t, err) 75 | 76 | // wait for the metric to be recalculated from query 77 | time.Sleep(time.Duration(5) * time.Second) 78 | out, err = prometheus.DefaultGatherer.Gather() 79 | require.NotNil(t, out) 80 | require.NoError(t, err) 81 | 82 | metric = filterMetrics(t, out, []string{"postgres1_template_count"}) 83 | require.NotNil(t, metric) 84 | require.Equal(t, 1, len(metric)) 85 | require.NotZero(t, metric[0].Metric[0].Gauge.Value) 86 | // check the value is not equal to old value 87 | require.NotEqual(t, *currentValue, *metric[0].Metric[0].Gauge.Value) 88 | 89 | // TearDown 90 | res, err = db.Exec(dropTableStmt) 91 | require.NoError(t, err) 92 | require.NotNil(t, res) 93 | 94 | } 95 | 96 | func filterMetrics(t *testing.T, metrics []*dto.MetricFamily, names []string) []*dto.MetricFamily { 97 | 98 | t.Helper() 99 | 100 | var filtered []*dto.MetricFamily 101 | for _, m := range metrics { 102 | drop := true 103 | for _, name := range names { 104 | if m.GetName() == name { 105 | drop = false 106 | break 107 | } 108 | } 109 | if !drop { 110 | filtered = append(filtered, m) 111 | } 112 | } 113 | return filtered 114 | } 115 | -------------------------------------------------------------------------------- /test/invalid_config.yaml: -------------------------------------------------------------------------------- 1 | connections: 2 | - name: mongodb1 3 | type: MONGO 4 | connectionStringFromEnv: MONGO_CONN 5 | metrics: 6 | - name: active_user_count 7 | helpString: users in the product 8 | database: yolo_db 9 | collection: user 10 | query: '{"is_active":true}' 11 | time: 60 12 | - name: total_user_count 13 | helpString: users in the product 14 | database: yolo_db 15 | collection: user 16 | query: "" 17 | time: 120 18 | - name: postgres1 19 | type: SQL 20 | connectionStringFromEnv: POSTGRES_CONN 21 | metrics: 22 | - name: template_count 23 | helpString: products in the db 24 | query: select * from templates 25 | time: 2 26 | - name: active_template_count 27 | helpString: products in the db 28 | query: error 29 | time: 4 30 | -------------------------------------------------------------------------------- /test/test_config.yaml: -------------------------------------------------------------------------------- 1 | connections: 2 | - name: mongodb1 3 | type: MONGO 4 | connectionStringFromEnv: TEST_MONGO_CONN 5 | metrics: 6 | - name: active_user_count 7 | helpString: users in the product 8 | database: yolo_db 9 | collection: user 10 | query: '{"is_active":true}' 11 | time: 60 12 | - name: total_user_count 13 | helpString: users in the product 14 | database: yolo_db 15 | collection: user 16 | query: "" 17 | time: 120 18 | - name: postgres1 19 | type: SQL 20 | connectionStringFromEnv: TEST_SQL_CONN 21 | metrics: 22 | - name: template_count 23 | helpString: products in the db 24 | query: select * from queries 25 | time: 2 26 | --------------------------------------------------------------------------------