From c394a61a4e1fe395191262ed1ccaf70b90a71484 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Guillot?= Date: Sun, 27 Sep 2020 16:01:06 -0700 Subject: [PATCH] Add Prometheus exporter --- Makefile | 2 +- cli/daemon.go | 20 +- config/options.go | 27 +++ config/parser.go | 20 ++ go.mod | 11 +- go.sum | 374 +++++++++++++++++++++++++++++++++ metric/metric.go | 120 +++++++++++ miniflux.1 | 11 + reader/feed/handler.go | 2 +- reader/processor/processor.go | 35 ++- reader/scraper/scraper.go | 4 +- reader/scraper/scraper_test.go | 10 +- service/httpd/httpd.go | 38 ++++ service/scheduler/scheduler.go | 17 +- storage/entry.go | 28 +++ storage/feed.go | 49 ++++- storage/user.go | 11 + ui/about.go | 4 +- ui/api_key_create.go | 2 +- ui/api_key_list.go | 2 +- ui/api_key_save.go | 2 +- ui/bookmark_entries.go | 2 +- ui/category_create.go | 2 +- ui/category_edit.go | 2 +- ui/category_entries.go | 2 +- ui/category_entries_all.go | 2 +- ui/category_feeds.go | 2 +- ui/category_list.go | 4 +- ui/category_save.go | 4 +- ui/category_update.go | 2 +- ui/entry_bookmark.go | 2 +- ui/entry_category.go | 2 +- ui/entry_feed.go | 2 +- ui/entry_read.go | 2 +- ui/entry_search.go | 2 +- ui/entry_unread.go | 2 +- ui/feed_edit.go | 2 +- ui/feed_entries.go | 2 +- ui/feed_entries_all.go | 2 +- ui/feed_list.go | 2 +- ui/feed_update.go | 2 +- ui/history_entries.go | 2 +- ui/integration_show.go | 2 +- ui/opml_import.go | 2 +- ui/opml_upload.go | 4 +- ui/search_entries.go | 2 +- ui/session_list.go | 2 +- ui/settings_show.go | 2 +- ui/settings_update.go | 4 +- ui/shared_entries.go | 2 +- ui/subscription_add.go | 2 +- ui/subscription_bookmarklet.go | 2 +- ui/subscription_choose.go | 2 +- ui/subscription_submit.go | 4 +- ui/unread_entries.go | 2 +- ui/user_create.go | 2 +- ui/user_edit.go | 2 +- ui/user_list.go | 4 +- ui/user_save.go | 6 +- ui/user_update.go | 2 +- worker/worker.go | 22 +- 61 files changed, 809 insertions(+), 96 deletions(-) create mode 100644 metric/metric.go diff --git a/Makefile b/Makefile index aeb42e86..7161ae8e 100644 --- a/Makefile +++ b/Makefile @@ -92,7 +92,7 @@ windows-x86: generate @ GOOS=windows GOARCH=386 go build -ldflags=$(LD_FLAGS) -o $(APP)-windows-x86 main.go run: generate - @ go run main.go -debug + @ LOG_DATE_TIME=1 go run main.go -debug clean: @ rm -f $(APP)-* $(APP) diff --git a/cli/daemon.go b/cli/daemon.go index 52039452..2ee6392e 100644 --- a/cli/daemon.go +++ b/cli/daemon.go @@ -9,12 +9,12 @@ import ( "net/http" "os" "os/signal" - "runtime" "syscall" "time" "miniflux.app/config" "miniflux.app/logger" + "miniflux.app/metric" "miniflux.app/reader/feed" "miniflux.app/service/httpd" "miniflux.app/service/scheduler" @@ -32,8 +32,6 @@ func startDaemon(store *storage.Storage) { feedHandler := feed.NewFeedHandler(store) pool := worker.NewPool(feedHandler, config.Opts.WorkerPoolSize()) - go showProcessStatistics() - if config.Opts.HasSchedulerService() && !config.Opts.HasMaintenanceMode() { scheduler.Serve(store, pool) } @@ -43,6 +41,11 @@ func startDaemon(store *storage.Storage) { httpServer = httpd.Serve(store, pool, feedHandler) } + if config.Opts.HasMetricsCollector() { + collector := metric.NewCollector(store, config.Opts.MetricsRefreshInterval()) + go collector.GatherStorageMetrics() + } + <-stop logger.Info("Shutting down the process...") ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) @@ -54,14 +57,3 @@ func startDaemon(store *storage.Storage) { logger.Info("Process gracefully stopped") } - -func showProcessStatistics() { - for { - var m runtime.MemStats - runtime.ReadMemStats(&m) - logger.Debug("Sys=%vK, InUse=%vK, HeapInUse=%vK, StackSys=%vK, StackInUse=%vK, GoRoutines=%d, NumCPU=%d", - m.Sys/1024, (m.Sys-m.HeapReleased)/1024, m.HeapInuse/1024, m.StackSys/1024, m.StackInuse/1024, - runtime.NumGoroutine(), runtime.NumCPU()) - time.Sleep(30 * time.Second) - } -} diff --git a/config/options.go b/config/options.go index 0934f83c..f7d7bda0 100644 --- a/config/options.go +++ b/config/options.go @@ -56,6 +56,9 @@ const ( defaultAuthProxyUserCreation = false defaultMaintenanceMode = false defaultMaintenanceMessage = "Miniflux is currently under maintenance" + defaultMetricsCollector = false + defaultMetricsRefreshInterval = 60 + defaultMetricsAllowedNetworks = "127.0.0.1/8" ) // Options contains configuration options. @@ -106,6 +109,9 @@ type Options struct { authProxyUserCreation bool maintenanceMode bool maintenanceMessage string + metricsCollector bool + metricsRefreshInterval int + metricsAllowedNetworks []string } // NewOptions returns Options with default values. @@ -155,6 +161,9 @@ func NewOptions() *Options { authProxyUserCreation: defaultAuthProxyUserCreation, maintenanceMode: defaultMaintenanceMode, maintenanceMessage: defaultMaintenanceMessage, + metricsCollector: defaultMetricsCollector, + metricsRefreshInterval: defaultMetricsRefreshInterval, + metricsAllowedNetworks: []string{defaultMetricsAllowedNetworks}, } } @@ -398,6 +407,21 @@ func (o *Options) IsAuthProxyUserCreationAllowed() bool { return o.authProxyUserCreation } +// HasMetricsCollector returns true if metrics collection is enabled. +func (o *Options) HasMetricsCollector() bool { + return o.metricsCollector +} + +// MetricsRefreshInterval returns the refresh interval in seconds. +func (o *Options) MetricsRefreshInterval() int { + return o.metricsRefreshInterval +} + +// MetricsAllowedNetworks returns the list of networks allowed to connect to the metrics endpoint. +func (o *Options) MetricsAllowedNetworks() []string { + return o.metricsAllowedNetworks +} + func (o *Options) String() string { var builder strings.Builder builder.WriteString(fmt.Sprintf("LOG_DATE_TIME: %v\n", o.logDateTime)) @@ -446,5 +470,8 @@ func (o *Options) String() string { builder.WriteString(fmt.Sprintf("AUTH_PROXY_USER_CREATION: %v\n", o.authProxyUserCreation)) builder.WriteString(fmt.Sprintf("MAINTENANCE_MODE: %v\n", o.maintenanceMode)) builder.WriteString(fmt.Sprintf("MAINTENANCE_MESSAGE: %v\n", o.maintenanceMessage)) + builder.WriteString(fmt.Sprintf("METRICS_COLLECTOR: %v\n", o.metricsCollector)) + builder.WriteString(fmt.Sprintf("METRICS_REFRESH_INTERVAL: %v\n", o.metricsRefreshInterval)) + builder.WriteString(fmt.Sprintf("METRICS_ALLOWED_NETWORKS: %v\n", o.metricsAllowedNetworks)) return builder.String() } diff --git a/config/parser.go b/config/parser.go index fdad597d..0f4f40d4 100644 --- a/config/parser.go +++ b/config/parser.go @@ -178,6 +178,12 @@ func (p *Parser) parseLines(lines []string) (err error) { p.opts.maintenanceMode = parseBool(value, defaultMaintenanceMode) case "MAINTENANCE_MESSAGE": p.opts.maintenanceMessage = parseString(value, defaultMaintenanceMessage) + case "METRICS_COLLECTOR": + p.opts.metricsCollector = parseBool(value, defaultMetricsCollector) + case "METRICS_REFRESH_INTERVAL": + p.opts.metricsRefreshInterval = parseInt(value, defaultMetricsRefreshInterval) + case "METRICS_ALLOWED_NETWORKS": + p.opts.metricsAllowedNetworks = parseStringList(value, []string{defaultMetricsAllowedNetworks}) } } @@ -244,6 +250,20 @@ func parseString(value string, fallback string) string { return value } +func parseStringList(value string, fallback []string) []string { + if value == "" { + return fallback + } + + var strList []string + items := strings.Split(value, ",") + for _, item := range items { + strList = append(strList, strings.TrimSpace(item)) + } + + return strList +} + func readSecretFile(filename, fallback string) string { data, err := ioutil.ReadFile(filename) if err != nil { diff --git a/go.mod b/go.mod index 4b997f92..19d637d3 100644 --- a/go.mod +++ b/go.mod @@ -5,18 +5,21 @@ module miniflux.app require ( github.com/PuerkitoBio/goquery v1.5.1 github.com/coreos/go-oidc v2.2.1+incompatible - github.com/golang/protobuf v1.4.0 // indirect github.com/gorilla/mux v1.8.0 github.com/lib/pq v1.8.0 github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect + github.com/prometheus/client_golang v1.7.1 + github.com/prometheus/common v0.14.0 // indirect + github.com/prometheus/procfs v0.2.0 // indirect github.com/rylans/getlang v0.0.0-20200505200108-4c3188ff8a2d github.com/stretchr/testify v1.6.1 // indirect github.com/tdewolff/minify/v2 v2.9.5 // indirect - golang.org/x/crypto v0.0.0-20200423211502-4bdfaf469ed5 - golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0 + golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 + golang.org/x/net v0.0.0-20200625001655-4c5254603344 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d - golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c // indirect + golang.org/x/sys v0.0.0-20200926100807-9d91bd62050c // indirect google.golang.org/appengine v1.6.6 // indirect + google.golang.org/protobuf v1.25.0 // indirect gopkg.in/square/go-jose.v2 v2.5.0 // indirect ) diff --git a/go.sum b/go.sum index 3f9c4ea0..53306b7f 100644 --- a/go.sum +++ b/go.sum @@ -1,41 +1,282 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/PuerkitoBio/goquery v1.5.1 h1:PSPBGne8NIUWw+/7vFBV+kG2J/5MOjbzc7154OaKCSE= github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= +github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= +github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= +github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo= github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= +github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= +github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= +github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= +github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927/go.mod h1:h/aW8ynjgkuj+NQRlZcDbAbM1ORAbXjXX77sX7T289U= +github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjsz9N1i1YxjHAk= github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= +github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= +github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0 h1:oOuy+ugB+P/kBdUnG5QaMXSIyJ1q38wWSojYCb3z5VQ= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= +github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/lib/pq v1.8.0 h1:9xohqzkUwzR4Ga4ivdTcawVS89YSDVxXMa3xJX3cGzg= github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= +github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= +github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2/go.mod h1:0KeJpeMD6o+O4hW7qJOT7vyQPKrWmj26uf5wMc/IiIs= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= +github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= +github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= +github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= +github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= +github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= +github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= +github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= +github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= +github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= +github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU= github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= +github.com/prometheus/client_golang v1.7.1 h1:NTGy1Ja9pByO+xAeH/qiWnLrKtr3hJPNjaVUwnjpdpA= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= +github.com/prometheus/common v0.10.0 h1:RyRA7RzGXQZiW+tGMr7sxa85G1z0yOpM1qq5c8lNawc= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.14.0 h1:RHRyE8UocrbjU+6UvRzwi6HjiDfxrrBU91TtbKzkGp4= +github.com/prometheus/common v0.14.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.1.3 h1:F0+tqvhOksq22sc6iCHF5WGlWjdwj92p0udFh1VFBS8= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.2.0 h1:wH4vA7pcjKuZzjF7lM8awk4fnuJO6idemZXoKnULUx4= +github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/rylans/getlang v0.0.0-20200505200108-4c3188ff8a2d h1:4Jn2kzSKcpxxwpJdyWxfNWKCYe4Uki2VibkExYmBLiA= github.com/rylans/getlang v0.0.0-20200505200108-4c3188ff8a2d/go.mod h1:3vfmZI6aJd5Rb9W2TQ0Nmupl+qem21R05+hmCscI0Bk= +github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/tdewolff/minify v1.1.0 h1:nxHQi1ML+g3ZbZHffiZ6eC7vMqNvSRfX3KB5Y5y/kfw= @@ -49,49 +290,182 @@ github.com/tdewolff/parse/v2 v2.5.2/go.mod h1:WzaJpRSbwq++EIQHYIRTpbYKNA3gn9it1I github.com/tdewolff/parse/v2 v2.5.3 h1:fnPIstKgEfxd3+wwHnH73sAYydsR0o/jYhcQ6c5PkrA= github.com/tdewolff/parse/v2 v2.5.3/go.mod h1:WzaJpRSbwq++EIQHYIRTpbYKNA3gn9it1Ik++q4zyho= github.com/tdewolff/test v1.0.6/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= +go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200423211502-4bdfaf469ed5 h1:Q7tZBpemrlsc2I7IyODzhtallWRSm4Q0d09pL6XbQtU= golang.org/x/crypto v0.0.0-20200423211502-4bdfaf469ed5/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0 h1:Jcxah/M+oLZ/R4/z5RzfPzGbPXnVDPkEDtf2JnuxN+U= golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c h1:UIcGWL6/wpCfyGuJnRFJRurA+yj8RrW7Q6x2YMCXt6c= golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200926100807-9d91bd62050c h1:38q6VNPWR010vN82/SB121GujZNIfAUb4YttE2rhGuc= +golang.org/x/sys v0.0.0-20200926100807-9d91bd62050c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0 h1:qdOKuR/EIArgaWNjetjgTzgVTAZ+S/WXVrq9HW9zimw= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/square/go-jose.v2 v2.5.0 h1:OZ4sdq+Y+SHfYB7vfthi1Ei8b0vkP8ZPQgUfUwdUSqo= gopkg.in/square/go-jose.v2 v2.5.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= diff --git a/metric/metric.go b/metric/metric.go new file mode 100644 index 00000000..8e1c972e --- /dev/null +++ b/metric/metric.go @@ -0,0 +1,120 @@ +// Copyright 2020 Frédéric Guillot. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +package metric // import "miniflux.app/metric" + +import ( + "time" + + "miniflux.app/logger" + "miniflux.app/storage" + + "github.com/prometheus/client_golang/prometheus" +) + +// Prometheus Metrics. +var ( + BackgroundFeedRefreshDuration = prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Namespace: "miniflux", + Name: "background_feed_refresh_duration", + Help: "Processing time to refresh feeds from the background workers", + Buckets: prometheus.LinearBuckets(1, 2, 15), + }, + []string{"status"}, + ) + + ScraperRequestDuration = prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Namespace: "miniflux", + Name: "scraper_request_duration", + Help: "Web scraper request duration", + Buckets: prometheus.LinearBuckets(1, 2, 25), + }, + []string{"status"}, + ) + + ArchiveEntriesDuration = prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Namespace: "miniflux", + Name: "archive_entries_duration", + Help: "Archive entries duration", + Buckets: prometheus.LinearBuckets(1, 2, 30), + }, + []string{"status"}, + ) + + usersGauge = prometheus.NewGauge( + prometheus.GaugeOpts{ + Namespace: "miniflux", + Name: "users", + Help: "Number of users", + }, + ) + + feedsGauge = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: "miniflux", + Name: "feeds", + Help: "Number of feeds by status", + }, + []string{"status"}, + ) + + brokenFeedsGauge = prometheus.NewGauge( + prometheus.GaugeOpts{ + Namespace: "miniflux", + Name: "broken_feeds", + Help: "Number of broken feeds", + }, + ) + + entriesGauge = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: "miniflux", + Name: "entries", + Help: "Number of entries by status", + }, + []string{"status"}, + ) +) + +// Collector represents a metric collector. +type Collector struct { + store *storage.Storage + refreshInterval int +} + +// NewCollector initializes a new metric collector. +func NewCollector(store *storage.Storage, refreshInterval int) *Collector { + prometheus.MustRegister(BackgroundFeedRefreshDuration) + prometheus.MustRegister(ScraperRequestDuration) + prometheus.MustRegister(ArchiveEntriesDuration) + prometheus.MustRegister(usersGauge) + prometheus.MustRegister(feedsGauge) + prometheus.MustRegister(brokenFeedsGauge) + prometheus.MustRegister(entriesGauge) + + return &Collector{store, refreshInterval} +} + +// GatherStorageMetrics polls the database to fetch metrics. +func (c *Collector) GatherStorageMetrics() { + for range time.Tick(time.Duration(c.refreshInterval) * time.Second) { + logger.Debug("[Metric] Collecting database metrics") + + usersGauge.Set(float64(c.store.CountUsers())) + brokenFeedsGauge.Set(float64(c.store.CountAllFeedsWithErrors())) + + feedsCount := c.store.CountAllFeeds() + for status, count := range feedsCount { + feedsGauge.WithLabelValues(status).Set(float64(count)) + } + + entriesCount := c.store.CountAllEntries() + for status, count := range entriesCount { + entriesGauge.WithLabelValues(status).Set(float64(count)) + } + } +} diff --git a/miniflux.1 b/miniflux.1 index 8ca59445..bc9d25e0 100644 --- a/miniflux.1 +++ b/miniflux.1 @@ -192,6 +192,17 @@ Use Let's Encrypt to get automatically a certificate for this domain\&. .B CERT_CACHE Let's Encrypt cache directory (default is /tmp/cert_cache)\&. .TP +.B METRICS_COLLECTOR +Toggle metrics collector. Disabled by default\&. +.TP +.B METRICS_REFRESH_INTERVAL +Refresh interval to collect database metrics\&. Default is 60 seconds\&. +.TP +.B METRICS_ALLOWED_NETWORKS +List of networks allowed to access the metrics endpoint (comma-separated values)\&. +.br +Default is 127.0.0.1/8\&. +.TP .B OAUTH2_PROVIDER OAuth2 provider to use\&. Only google is supported\&. .TP diff --git a/reader/feed/handler.go b/reader/feed/handler.go index 20f19ce9..5c81d08a 100644 --- a/reader/feed/handler.go +++ b/reader/feed/handler.go @@ -81,7 +81,7 @@ func (h *Handler) CreateFeed(userID, categoryID int64, url string, crawler bool, return subscription, nil } -// RefreshFeed fetch and update a feed if necessary. +// RefreshFeed refreshes a feed. func (h *Handler) RefreshFeed(userID, feedID int64) error { defer timer.ExecutionTime(time.Now(), fmt.Sprintf("[Handler:RefreshFeed] feedID=%d", feedID)) userLanguage := h.store.UserLanguage(userID) diff --git a/reader/processor/processor.go b/reader/processor/processor.go index 68cae2df..81145176 100644 --- a/reader/processor/processor.go +++ b/reader/processor/processor.go @@ -5,7 +5,11 @@ package processor import ( + "time" + + "miniflux.app/config" "miniflux.app/logger" + "miniflux.app/metric" "miniflux.app/model" "miniflux.app/reader/rewrite" "miniflux.app/reader/sanitizer" @@ -20,9 +24,19 @@ func ProcessFeedEntries(store *storage.Storage, feed *model.Feed) { if feed.Crawler { if !store.EntryURLExists(feed.ID, entry.URL) { - content, err := scraper.Fetch(entry.URL, feed.ScraperRules, feed.UserAgent) - if err != nil { - logger.Error(`[Filter] Unable to crawl this entry: %q => %v`, entry.URL, err) + startTime := time.Now() + content, scraperErr := scraper.Fetch(entry.URL, feed.ScraperRules, feed.UserAgent) + + if config.Opts.HasMetricsCollector() { + status := "success" + if scraperErr != nil { + status = "error" + } + metric.ScraperRequestDuration.WithLabelValues(status).Observe(time.Since(startTime).Seconds()) + } + + if scraperErr != nil { + logger.Error(`[Filter] Unable to crawl this entry: %q => %v`, entry.URL, scraperErr) } else if content != "" { // We replace the entry content only if the scraper doesn't return any error. entry.Content = content @@ -39,9 +53,18 @@ func ProcessFeedEntries(store *storage.Storage, feed *model.Feed) { // ProcessEntryWebPage downloads the entry web page and apply rewrite rules. func ProcessEntryWebPage(entry *model.Entry) error { - content, err := scraper.Fetch(entry.URL, entry.Feed.ScraperRules, entry.Feed.UserAgent) - if err != nil { - return err + startTime := time.Now() + content, scraperErr := scraper.Fetch(entry.URL, entry.Feed.ScraperRules, entry.Feed.UserAgent) + if config.Opts.HasMetricsCollector() { + status := "success" + if scraperErr != nil { + status = "error" + } + metric.ScraperRequestDuration.WithLabelValues(status).Observe(time.Since(startTime).Seconds()) + } + + if scraperErr != nil { + return scraperErr } content = rewrite.Rewriter(entry.URL, content, entry.Feed.RewriteRules) diff --git a/reader/scraper/scraper.go b/reader/scraper/scraper.go index c0b968a7..f1b7e244 100644 --- a/reader/scraper/scraper.go +++ b/reader/scraper/scraper.go @@ -35,7 +35,7 @@ func Fetch(websiteURL, rules, userAgent string) (string, error) { return "", errors.New("scraper: unable to download web page") } - if !isWhitelistedContentType(response.ContentType) { + if !isAllowedContentType(response.ContentType) { return "", fmt.Errorf("scraper: this resource is not a HTML document (%s)", response.ContentType) } @@ -95,7 +95,7 @@ func getPredefinedScraperRules(websiteURL string) string { return "" } -func isWhitelistedContentType(contentType string) bool { +func isAllowedContentType(contentType string) bool { contentType = strings.ToLower(contentType) return strings.HasPrefix(contentType, "text/html") || strings.HasPrefix(contentType, "application/xhtml+xml") diff --git a/reader/scraper/scraper_test.go b/reader/scraper/scraper_test.go index 14e13ee8..5b1414df 100644 --- a/reader/scraper/scraper_test.go +++ b/reader/scraper/scraper_test.go @@ -39,7 +39,7 @@ func TestWhitelistedContentTypes(t *testing.T) { } for inputValue, expectedResult := range scenarios { - actualResult := isWhitelistedContentType(inputValue) + actualResult := isAllowedContentType(inputValue) if actualResult != expectedResult { t.Errorf(`Unexpected result for content type whitelist, got "%v" instead of "%v"`, actualResult, expectedResult) } @@ -47,10 +47,10 @@ func TestWhitelistedContentTypes(t *testing.T) { } func TestSelectorRules(t *testing.T) { - var ruleTestCases = map[string]string { - "img.html": "article > img", - "iframe.html": "article > iframe", - "p.html": "article > p", + var ruleTestCases = map[string]string{ + "img.html": "article > img", + "iframe.html": "article > iframe", + "p.html": "article > p", } for filename, rule := range ruleTestCases { diff --git a/service/httpd/httpd.go b/service/httpd/httpd.go index aa9ceca3..36e97326 100644 --- a/service/httpd/httpd.go +++ b/service/httpd/httpd.go @@ -16,6 +16,7 @@ import ( "miniflux.app/api" "miniflux.app/config" "miniflux.app/fever" + "miniflux.app/http/request" "miniflux.app/logger" "miniflux.app/reader/feed" "miniflux.app/storage" @@ -24,6 +25,7 @@ import ( "miniflux.app/worker" "github.com/gorilla/mux" + "github.com/prometheus/client_golang/prometheus/promhttp" "golang.org/x/crypto/acme/autocert" ) @@ -186,9 +188,45 @@ func setupHandler(store *storage.Storage, feedHandler *feed.Handler, pool *worke router.HandleFunc("/healthcheck", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("OK")) }).Name("healthcheck") + router.HandleFunc("/version", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(version.Version)) }).Name("version") + if config.Opts.HasMetricsCollector() { + router.Handle("/metrics", promhttp.Handler()).Name("metrics") + router.Use(func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + route := mux.CurrentRoute(r) + + // Returns a 404 if the client is not authorized to access the metrics endpoint. + if route.GetName() == "metrics" && !isAllowedToAccessMetricsEndpoint(r) { + logger.Error(`[Metrics] Client not allowed: %s`, request.ClientIP(r)) + http.NotFound(w, r) + return + } + + next.ServeHTTP(w, r) + }) + }) + } + return router } + +func isAllowedToAccessMetricsEndpoint(r *http.Request) bool { + clientIP := net.ParseIP(request.ClientIP(r)) + + for _, cidr := range config.Opts.MetricsAllowedNetworks() { + _, network, err := net.ParseCIDR(cidr) + if err != nil { + logger.Fatal(`[Metrics] Unable to parse CIDR %v`, err) + } + + if network.Contains(clientIP) { + return true + } + } + + return false +} diff --git a/service/scheduler/scheduler.go b/service/scheduler/scheduler.go index 9e822705..6ec55279 100644 --- a/service/scheduler/scheduler.go +++ b/service/scheduler/scheduler.go @@ -9,6 +9,7 @@ import ( "miniflux.app/config" "miniflux.app/logger" + "miniflux.app/metric" "miniflux.app/model" "miniflux.app/storage" "miniflux.app/worker" @@ -35,8 +36,7 @@ func Serve(store *storage.Storage, pool *worker.Pool) { } func feedScheduler(store *storage.Storage, pool *worker.Pool, frequency, batchSize int) { - c := time.Tick(time.Duration(frequency) * time.Minute) - for range c { + for range time.Tick(time.Duration(frequency) * time.Minute) { jobs, err := store.NewBatch(batchSize) if err != nil { logger.Error("[Scheduler:Feed] %v", err) @@ -48,22 +48,31 @@ func feedScheduler(store *storage.Storage, pool *worker.Pool, frequency, batchSi } func cleanupScheduler(store *storage.Storage, frequency, archiveReadDays, archiveUnreadDays, sessionsDays int) { - c := time.Tick(time.Duration(frequency) * time.Hour) - for range c { + for range time.Tick(time.Duration(frequency) * time.Hour) { nbSessions := store.CleanOldSessions(sessionsDays) nbUserSessions := store.CleanOldUserSessions(sessionsDays) logger.Info("[Scheduler:Cleanup] Cleaned %d sessions and %d user sessions", nbSessions, nbUserSessions) + startTime := time.Now() if rowsAffected, err := store.ArchiveEntries(model.EntryStatusRead, archiveReadDays); err != nil { logger.Error("[Scheduler:ArchiveReadEntries] %v", err) } else { logger.Info("[Scheduler:ArchiveReadEntries] %d entries changed", rowsAffected) + + if config.Opts.HasMetricsCollector() { + metric.ArchiveEntriesDuration.WithLabelValues(model.EntryStatusRead).Observe(time.Since(startTime).Seconds()) + } } + startTime = time.Now() if rowsAffected, err := store.ArchiveEntries(model.EntryStatusUnread, archiveUnreadDays); err != nil { logger.Error("[Scheduler:ArchiveUnreadEntries] %v", err) } else { logger.Info("[Scheduler:ArchiveUnreadEntries] %d entries changed", rowsAffected) + + if config.Opts.HasMetricsCollector() { + metric.ArchiveEntriesDuration.WithLabelValues(model.EntryStatusUnread).Observe(time.Since(startTime).Seconds()) + } } } } diff --git a/storage/entry.go b/storage/entry.go index 556f526c..e1a85af4 100644 --- a/storage/entry.go +++ b/storage/entry.go @@ -17,6 +17,34 @@ import ( "github.com/lib/pq" ) +// CountAllEntries returns the number of entries for each status in the database. +func (s *Storage) CountAllEntries() map[string]int64 { + rows, err := s.db.Query(`SELECT status, count(*) FROM entries GROUP BY status`) + if err != nil { + return nil + } + defer rows.Close() + + results := make(map[string]int64) + results["unread"] = 0 + results["read"] = 0 + results["removed"] = 0 + + for rows.Next() { + var status string + var count int64 + + if err := rows.Scan(&status, &count); err != nil { + continue + } + + results[status] = count + } + + results["total"] = results["unread"] + results["read"] + results["removed"] + return results +} + // CountUnreadEntries returns the number of unread entries. func (s *Storage) CountUnreadEntries(userID int64) int { builder := s.NewEntryQueryBuilder(userID) diff --git a/storage/feed.go b/storage/feed.go index 55ce6e83..697ef095 100644 --- a/storage/feed.go +++ b/storage/feed.go @@ -76,6 +76,37 @@ func (s *Storage) AnotherFeedURLExists(userID, feedID int64, feedURL string) boo return result } +// CountAllFeeds returns the number of feeds in the database. +func (s *Storage) CountAllFeeds() map[string]int64 { + rows, err := s.db.Query(`SELECT disabled, count(*) FROM feeds GROUP BY disabled`) + if err != nil { + return nil + } + defer rows.Close() + + results := make(map[string]int64) + results["enabled"] = 0 + results["disabled"] = 0 + + for rows.Next() { + var disabled bool + var count int64 + + if err := rows.Scan(&disabled, &count); err != nil { + continue + } + + if disabled { + results["disabled"] = count + } else { + results["enabled"] = count + } + } + + results["total"] = results["disabled"] + results["enabled"] + return results +} + // CountFeeds returns the number of feeds that belongs to the given user. func (s *Storage) CountFeeds(userID int64) int { var result int @@ -87,9 +118,9 @@ func (s *Storage) CountFeeds(userID int64) int { return result } -// CountErrorFeeds returns the number of feeds with parse errors that belong to the given user. -func (s *Storage) CountErrorFeeds(userID int64) int { - query := `SELECT count(*) FROM feeds WHERE user_id=$1 AND parsing_error_count>=$2` +// CountUserFeedsWithErrors returns the number of feeds with parsing errors that belong to the given user. +func (s *Storage) CountUserFeedsWithErrors(userID int64) int { + query := `SELECT count(*) FROM feeds WHERE user_id=$1 AND parsing_error_count >= $2` var result int err := s.db.QueryRow(query, userID, maxParsingError).Scan(&result) if err != nil { @@ -99,6 +130,18 @@ func (s *Storage) CountErrorFeeds(userID int64) int { return result } +// CountAllFeedsWithErrors returns the number of feeds with parsing errors. +func (s *Storage) CountAllFeedsWithErrors() int { + query := `SELECT count(*) FROM feeds WHERE parsing_error_count >= $1` + var result int + err := s.db.QueryRow(query, maxParsingError).Scan(&result) + if err != nil { + return 0 + } + + return result +} + // Feeds returns all feeds that belongs to the given user. func (s *Storage) Feeds(userID int64) (model.Feeds, error) { return s.fetchFeeds(feedListQuery, "", userID) diff --git a/storage/user.go b/storage/user.go index 9d3a8c6a..46ff9386 100644 --- a/storage/user.go +++ b/storage/user.go @@ -16,6 +16,17 @@ import ( "golang.org/x/crypto/bcrypt" ) +// CountUsers returns the total number of users. +func (s *Storage) CountUsers() int { + var result int + err := s.db.QueryRow(`SELECT count(*) FROM users`).Scan(&result) + if err != nil { + return 0 + } + + return result +} + // SetLastLogin updates the last login date of a user. func (s *Storage) SetLastLogin(userID int64) error { query := `UPDATE users SET last_login_at=now() WHERE id=$1` diff --git a/ui/about.go b/ui/about.go index 5bb9f6b3..7a27c9ed 100644 --- a/ui/about.go +++ b/ui/about.go @@ -7,8 +7,8 @@ package ui // import "miniflux.app/ui" import ( "net/http" - "miniflux.app/http/response/html" "miniflux.app/http/request" + "miniflux.app/http/response/html" "miniflux.app/ui/session" "miniflux.app/ui/view" "miniflux.app/version" @@ -28,7 +28,7 @@ func (h *handler) showAboutPage(w http.ResponseWriter, r *http.Request) { view.Set("menu", "settings") view.Set("user", user) view.Set("countUnread", h.store.CountUnreadEntries(user.ID)) - view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID)) + view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID)) html.OK(w, r, view.Render("about")) } diff --git a/ui/api_key_create.go b/ui/api_key_create.go index 87f87394..c33f8d0e 100644 --- a/ui/api_key_create.go +++ b/ui/api_key_create.go @@ -28,7 +28,7 @@ func (h *handler) showCreateAPIKeyPage(w http.ResponseWriter, r *http.Request) { view.Set("menu", "settings") view.Set("user", user) view.Set("countUnread", h.store.CountUnreadEntries(user.ID)) - view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID)) + view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID)) html.OK(w, r, view.Render("create_api_key")) } diff --git a/ui/api_key_list.go b/ui/api_key_list.go index 2b4d06ab..0e6a8cd7 100644 --- a/ui/api_key_list.go +++ b/ui/api_key_list.go @@ -33,7 +33,7 @@ func (h *handler) showAPIKeysPage(w http.ResponseWriter, r *http.Request) { view.Set("menu", "settings") view.Set("user", user) view.Set("countUnread", h.store.CountUnreadEntries(user.ID)) - view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID)) + view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID)) html.OK(w, r, view.Render("api_keys")) } diff --git a/ui/api_key_save.go b/ui/api_key_save.go index f8206af0..4c7dfc59 100644 --- a/ui/api_key_save.go +++ b/ui/api_key_save.go @@ -32,7 +32,7 @@ func (h *handler) saveAPIKey(w http.ResponseWriter, r *http.Request) { view.Set("menu", "settings") view.Set("user", user) view.Set("countUnread", h.store.CountUnreadEntries(user.ID)) - view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID)) + view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID)) if err := apiKeyForm.Validate(); err != nil { view.Set("errorMessage", err.Error()) diff --git a/ui/bookmark_entries.go b/ui/bookmark_entries.go index 5bf107ea..2b8a31d8 100644 --- a/ui/bookmark_entries.go +++ b/ui/bookmark_entries.go @@ -52,7 +52,7 @@ func (h *handler) showStarredPage(w http.ResponseWriter, r *http.Request) { view.Set("menu", "starred") view.Set("user", user) view.Set("countUnread", h.store.CountUnreadEntries(user.ID)) - view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID)) + view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID)) view.Set("hasSaveEntry", h.store.HasSaveEntry(user.ID)) html.OK(w, r, view.Render("bookmark_entries")) diff --git a/ui/category_create.go b/ui/category_create.go index d8fe2cfa..c61b2407 100644 --- a/ui/category_create.go +++ b/ui/category_create.go @@ -25,7 +25,7 @@ func (h *handler) showCreateCategoryPage(w http.ResponseWriter, r *http.Request) view.Set("menu", "categories") view.Set("user", user) view.Set("countUnread", h.store.CountUnreadEntries(user.ID)) - view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID)) + view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID)) html.OK(w, r, view.Render("create_category")) } diff --git a/ui/category_edit.go b/ui/category_edit.go index 344cfe4e..f21a63a7 100644 --- a/ui/category_edit.go +++ b/ui/category_edit.go @@ -45,7 +45,7 @@ func (h *handler) showEditCategoryPage(w http.ResponseWriter, r *http.Request) { view.Set("menu", "categories") view.Set("user", user) view.Set("countUnread", h.store.CountUnreadEntries(user.ID)) - view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID)) + view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID)) html.OK(w, r, view.Render("edit_category")) } diff --git a/ui/category_entries.go b/ui/category_entries.go index 09a6fd16..48a28939 100644 --- a/ui/category_entries.go +++ b/ui/category_entries.go @@ -64,7 +64,7 @@ func (h *handler) showCategoryEntriesPage(w http.ResponseWriter, r *http.Request view.Set("menu", "categories") view.Set("user", user) view.Set("countUnread", h.store.CountUnreadEntries(user.ID)) - view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID)) + view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID)) view.Set("hasSaveEntry", h.store.HasSaveEntry(user.ID)) view.Set("showOnlyUnreadEntries", true) diff --git a/ui/category_entries_all.go b/ui/category_entries_all.go index ab2f725b..d6cc55f6 100644 --- a/ui/category_entries_all.go +++ b/ui/category_entries_all.go @@ -64,7 +64,7 @@ func (h *handler) showCategoryEntriesAllPage(w http.ResponseWriter, r *http.Requ view.Set("menu", "categories") view.Set("user", user) view.Set("countUnread", h.store.CountUnreadEntries(user.ID)) - view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID)) + view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID)) view.Set("hasSaveEntry", h.store.HasSaveEntry(user.ID)) view.Set("showOnlyUnreadEntries", false) diff --git a/ui/category_feeds.go b/ui/category_feeds.go index 202fc3e4..cbcce7bd 100644 --- a/ui/category_feeds.go +++ b/ui/category_feeds.go @@ -46,7 +46,7 @@ func (h *handler) showCategoryFeedsPage(w http.ResponseWriter, r *http.Request) view.Set("menu", "categories") view.Set("user", user) view.Set("countUnread", h.store.CountUnreadEntries(user.ID)) - view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID)) + view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID)) html.OK(w, r, view.Render("category_feeds")) } diff --git a/ui/category_list.go b/ui/category_list.go index 3137b956..1375888a 100644 --- a/ui/category_list.go +++ b/ui/category_list.go @@ -7,9 +7,9 @@ package ui // import "miniflux.app/ui" import ( "net/http" + "miniflux.app/http/request" "miniflux.app/http/response/html" "miniflux.app/ui/session" - "miniflux.app/http/request" "miniflux.app/ui/view" ) @@ -33,7 +33,7 @@ func (h *handler) showCategoryListPage(w http.ResponseWriter, r *http.Request) { view.Set("menu", "categories") view.Set("user", user) view.Set("countUnread", h.store.CountUnreadEntries(user.ID)) - view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID)) + view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID)) html.OK(w, r, view.Render("categories")) } diff --git a/ui/category_save.go b/ui/category_save.go index 48fe4b60..fe8f11d3 100644 --- a/ui/category_save.go +++ b/ui/category_save.go @@ -7,9 +7,9 @@ package ui // import "miniflux.app/ui" import ( "net/http" + "miniflux.app/http/request" "miniflux.app/http/response/html" "miniflux.app/http/route" - "miniflux.app/http/request" "miniflux.app/logger" "miniflux.app/model" "miniflux.app/ui/form" @@ -32,7 +32,7 @@ func (h *handler) saveCategory(w http.ResponseWriter, r *http.Request) { view.Set("menu", "categories") view.Set("user", user) view.Set("countUnread", h.store.CountUnreadEntries(user.ID)) - view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID)) + view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID)) if err := categoryForm.Validate(); err != nil { view.Set("errorMessage", err.Error()) diff --git a/ui/category_update.go b/ui/category_update.go index 591063ac..38f148df 100644 --- a/ui/category_update.go +++ b/ui/category_update.go @@ -44,7 +44,7 @@ func (h *handler) updateCategory(w http.ResponseWriter, r *http.Request) { view.Set("menu", "categories") view.Set("user", user) view.Set("countUnread", h.store.CountUnreadEntries(user.ID)) - view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID)) + view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID)) if err := categoryForm.Validate(); err != nil { view.Set("errorMessage", err.Error()) diff --git a/ui/entry_bookmark.go b/ui/entry_bookmark.go index a07859d7..86155713 100644 --- a/ui/entry_bookmark.go +++ b/ui/entry_bookmark.go @@ -77,7 +77,7 @@ func (h *handler) showStarredEntryPage(w http.ResponseWriter, r *http.Request) { view.Set("menu", "starred") view.Set("user", user) view.Set("countUnread", h.store.CountUnreadEntries(user.ID)) - view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID)) + view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID)) view.Set("hasSaveEntry", h.store.HasSaveEntry(user.ID)) html.OK(w, r, view.Render("entry")) diff --git a/ui/entry_category.go b/ui/entry_category.go index c2602572..d42dc020 100644 --- a/ui/entry_category.go +++ b/ui/entry_category.go @@ -80,7 +80,7 @@ func (h *handler) showCategoryEntryPage(w http.ResponseWriter, r *http.Request) view.Set("menu", "categories") view.Set("user", user) view.Set("countUnread", h.store.CountUnreadEntries(user.ID)) - view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID)) + view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID)) view.Set("hasSaveEntry", h.store.HasSaveEntry(user.ID)) html.OK(w, r, view.Render("entry")) diff --git a/ui/entry_feed.go b/ui/entry_feed.go index e4ea585d..6eebc905 100644 --- a/ui/entry_feed.go +++ b/ui/entry_feed.go @@ -80,7 +80,7 @@ func (h *handler) showFeedEntryPage(w http.ResponseWriter, r *http.Request) { view.Set("menu", "feeds") view.Set("user", user) view.Set("countUnread", h.store.CountUnreadEntries(user.ID)) - view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID)) + view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID)) view.Set("hasSaveEntry", h.store.HasSaveEntry(user.ID)) html.OK(w, r, view.Render("entry")) diff --git a/ui/entry_read.go b/ui/entry_read.go index df79d444..f67c0218 100644 --- a/ui/entry_read.go +++ b/ui/entry_read.go @@ -67,7 +67,7 @@ func (h *handler) showReadEntryPage(w http.ResponseWriter, r *http.Request) { view.Set("menu", "history") view.Set("user", user) view.Set("countUnread", h.store.CountUnreadEntries(user.ID)) - view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID)) + view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID)) view.Set("hasSaveEntry", h.store.HasSaveEntry(user.ID)) html.OK(w, r, view.Render("entry")) diff --git a/ui/entry_search.go b/ui/entry_search.go index d9955334..fcdb3aef 100644 --- a/ui/entry_search.go +++ b/ui/entry_search.go @@ -80,7 +80,7 @@ func (h *handler) showSearchEntryPage(w http.ResponseWriter, r *http.Request) { view.Set("menu", "search") view.Set("user", user) view.Set("countUnread", h.store.CountUnreadEntries(user.ID)) - view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID)) + view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID)) view.Set("hasSaveEntry", h.store.HasSaveEntry(user.ID)) html.OK(w, r, view.Render("entry")) diff --git a/ui/entry_unread.go b/ui/entry_unread.go index 414b8e88..af006ea8 100644 --- a/ui/entry_unread.go +++ b/ui/entry_unread.go @@ -84,7 +84,7 @@ func (h *handler) showUnreadEntryPage(w http.ResponseWriter, r *http.Request) { view.Set("menu", "unread") view.Set("user", user) view.Set("hasSaveEntry", h.store.HasSaveEntry(user.ID)) - view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID)) + view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID)) // Fetching the counter here avoid to be off by one. view.Set("countUnread", h.store.CountUnreadEntries(user.ID)) diff --git a/ui/feed_edit.go b/ui/feed_edit.go index 3dddb1b7..19905828 100644 --- a/ui/feed_edit.go +++ b/ui/feed_edit.go @@ -65,7 +65,7 @@ func (h *handler) showEditFeedPage(w http.ResponseWriter, r *http.Request) { view.Set("menu", "feeds") view.Set("user", user) view.Set("countUnread", h.store.CountUnreadEntries(user.ID)) - view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID)) + view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID)) view.Set("defaultUserAgent", client.DefaultUserAgent) view.Set("hasProxyConfigured", config.Opts.HasHTTPClientProxyConfigured()) diff --git a/ui/feed_entries.go b/ui/feed_entries.go index 30aafa4d..04b56e53 100644 --- a/ui/feed_entries.go +++ b/ui/feed_entries.go @@ -64,7 +64,7 @@ func (h *handler) showFeedEntriesPage(w http.ResponseWriter, r *http.Request) { view.Set("menu", "feeds") view.Set("user", user) view.Set("countUnread", h.store.CountUnreadEntries(user.ID)) - view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID)) + view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID)) view.Set("hasSaveEntry", h.store.HasSaveEntry(user.ID)) view.Set("showOnlyUnreadEntries", true) diff --git a/ui/feed_entries_all.go b/ui/feed_entries_all.go index 1ce3abec..a5b1d7d5 100644 --- a/ui/feed_entries_all.go +++ b/ui/feed_entries_all.go @@ -64,7 +64,7 @@ func (h *handler) showFeedEntriesAllPage(w http.ResponseWriter, r *http.Request) view.Set("menu", "feeds") view.Set("user", user) view.Set("countUnread", h.store.CountUnreadEntries(user.ID)) - view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID)) + view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID)) view.Set("hasSaveEntry", h.store.HasSaveEntry(user.ID)) view.Set("showOnlyUnreadEntries", false) diff --git a/ui/feed_list.go b/ui/feed_list.go index 7c7b9225..761c803e 100644 --- a/ui/feed_list.go +++ b/ui/feed_list.go @@ -33,7 +33,7 @@ func (h *handler) showFeedsPage(w http.ResponseWriter, r *http.Request) { view.Set("menu", "feeds") view.Set("user", user) view.Set("countUnread", h.store.CountUnreadEntries(user.ID)) - view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID)) + view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID)) html.OK(w, r, view.Render("feeds")) } diff --git a/ui/feed_update.go b/ui/feed_update.go index a5d830e9..81542c32 100644 --- a/ui/feed_update.go +++ b/ui/feed_update.go @@ -52,7 +52,7 @@ func (h *handler) updateFeed(w http.ResponseWriter, r *http.Request) { view.Set("menu", "feeds") view.Set("user", user) view.Set("countUnread", h.store.CountUnreadEntries(user.ID)) - view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID)) + view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID)) view.Set("defaultUserAgent", client.DefaultUserAgent) if err := feedForm.ValidateModification(); err != nil { diff --git a/ui/history_entries.go b/ui/history_entries.go index 7bee3946..efeb1d57 100644 --- a/ui/history_entries.go +++ b/ui/history_entries.go @@ -50,7 +50,7 @@ func (h *handler) showHistoryPage(w http.ResponseWriter, r *http.Request) { view.Set("menu", "history") view.Set("user", user) view.Set("countUnread", h.store.CountUnreadEntries(user.ID)) - view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID)) + view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID)) view.Set("hasSaveEntry", h.store.HasSaveEntry(user.ID)) html.OK(w, r, view.Render("history_entries")) diff --git a/ui/integration_show.go b/ui/integration_show.go index a43bb076..03d71e17 100644 --- a/ui/integration_show.go +++ b/ui/integration_show.go @@ -59,7 +59,7 @@ func (h *handler) showIntegrationPage(w http.ResponseWriter, r *http.Request) { view.Set("menu", "settings") view.Set("user", user) view.Set("countUnread", h.store.CountUnreadEntries(user.ID)) - view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID)) + view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID)) view.Set("hasPocketConsumerKeyConfigured", config.Opts.PocketConsumerKey("") != "") html.OK(w, r, view.Render("integrations")) diff --git a/ui/opml_import.go b/ui/opml_import.go index 06f54bb9..ea6b3626 100644 --- a/ui/opml_import.go +++ b/ui/opml_import.go @@ -25,7 +25,7 @@ func (h *handler) showImportPage(w http.ResponseWriter, r *http.Request) { view.Set("menu", "feeds") view.Set("user", user) view.Set("countUnread", h.store.CountUnreadEntries(user.ID)) - view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID)) + view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID)) html.OK(w, r, view.Render("import")) } diff --git a/ui/opml_upload.go b/ui/opml_upload.go index 6f9109bf..08430737 100644 --- a/ui/opml_upload.go +++ b/ui/opml_upload.go @@ -45,7 +45,7 @@ func (h *handler) uploadOPML(w http.ResponseWriter, r *http.Request) { view.Set("menu", "feeds") view.Set("user", user) view.Set("countUnread", h.store.CountUnreadEntries(user.ID)) - view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID)) + view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID)) if fileHeader.Size == 0 { view.Set("errorMessage", "error.empty_file") @@ -86,7 +86,7 @@ func (h *handler) fetchOPML(w http.ResponseWriter, r *http.Request) { view.Set("menu", "feeds") view.Set("user", user) view.Set("countUnread", h.store.CountUnreadEntries(user.ID)) - view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID)) + view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID)) clt := client.NewClientWithConfig(url, config.Opts) resp, err := clt.Get() diff --git a/ui/search_entries.go b/ui/search_entries.go index 054d7808..5dfd6db7 100644 --- a/ui/search_entries.go +++ b/ui/search_entries.go @@ -54,7 +54,7 @@ func (h *handler) showSearchEntriesPage(w http.ResponseWriter, r *http.Request) view.Set("menu", "search") view.Set("user", user) view.Set("countUnread", h.store.CountUnreadEntries(user.ID)) - view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID)) + view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID)) view.Set("hasSaveEntry", h.store.HasSaveEntry(user.ID)) html.OK(w, r, view.Render("search_entries")) diff --git a/ui/session_list.go b/ui/session_list.go index 28da2022..53051b2b 100644 --- a/ui/session_list.go +++ b/ui/session_list.go @@ -36,7 +36,7 @@ func (h *handler) showSessionsPage(w http.ResponseWriter, r *http.Request) { view.Set("menu", "settings") view.Set("user", user) view.Set("countUnread", h.store.CountUnreadEntries(user.ID)) - view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID)) + view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID)) html.OK(w, r, view.Render("sessions")) } diff --git a/ui/settings_show.go b/ui/settings_show.go index 8a8afbe7..a50f9760 100644 --- a/ui/settings_show.go +++ b/ui/settings_show.go @@ -51,7 +51,7 @@ func (h *handler) showSettingsPage(w http.ResponseWriter, r *http.Request) { view.Set("menu", "settings") view.Set("user", user) view.Set("countUnread", h.store.CountUnreadEntries(user.ID)) - view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID)) + view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID)) html.OK(w, r, view.Render("settings")) } diff --git a/ui/settings_update.go b/ui/settings_update.go index d4103d64..507fd833 100644 --- a/ui/settings_update.go +++ b/ui/settings_update.go @@ -7,8 +7,8 @@ package ui // import "miniflux.app/ui" import ( "net/http" - "miniflux.app/http/response/html" "miniflux.app/http/request" + "miniflux.app/http/response/html" "miniflux.app/http/route" "miniflux.app/locale" "miniflux.app/logger" @@ -43,7 +43,7 @@ func (h *handler) updateSettings(w http.ResponseWriter, r *http.Request) { view.Set("menu", "settings") view.Set("user", user) view.Set("countUnread", h.store.CountUnreadEntries(user.ID)) - view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID)) + view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID)) if err := settingsForm.Validate(); err != nil { view.Set("errorMessage", err.Error()) diff --git a/ui/shared_entries.go b/ui/shared_entries.go index 8d599441..0ba9cb19 100644 --- a/ui/shared_entries.go +++ b/ui/shared_entries.go @@ -45,7 +45,7 @@ func (h *handler) sharedEntries(w http.ResponseWriter, r *http.Request) { view.Set("menu", "history") view.Set("user", user) view.Set("countUnread", h.store.CountUnreadEntries(user.ID)) - view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID)) + view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID)) view.Set("hasSaveEntry", h.store.HasSaveEntry(user.ID)) html.OK(w, r, view.Render("shared_entries")) diff --git a/ui/subscription_add.go b/ui/subscription_add.go index e67f2510..920ceb40 100644 --- a/ui/subscription_add.go +++ b/ui/subscription_add.go @@ -36,7 +36,7 @@ func (h *handler) showAddSubscriptionPage(w http.ResponseWriter, r *http.Request view.Set("menu", "feeds") view.Set("user", user) view.Set("countUnread", h.store.CountUnreadEntries(user.ID)) - view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID)) + view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID)) view.Set("defaultUserAgent", client.DefaultUserAgent) view.Set("form", &form.SubscriptionForm{CategoryID: 0}) view.Set("hasProxyConfigured", config.Opts.HasHTTPClientProxyConfigured()) diff --git a/ui/subscription_bookmarklet.go b/ui/subscription_bookmarklet.go index bf1674ea..3b2c849e 100644 --- a/ui/subscription_bookmarklet.go +++ b/ui/subscription_bookmarklet.go @@ -39,7 +39,7 @@ func (h *handler) bookmarklet(w http.ResponseWriter, r *http.Request) { view.Set("menu", "feeds") view.Set("user", user) view.Set("countUnread", h.store.CountUnreadEntries(user.ID)) - view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID)) + view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID)) view.Set("defaultUserAgent", client.DefaultUserAgent) view.Set("hasProxyConfigured", config.Opts.HasHTTPClientProxyConfigured()) diff --git a/ui/subscription_choose.go b/ui/subscription_choose.go index 3df6e13a..68390e18 100644 --- a/ui/subscription_choose.go +++ b/ui/subscription_choose.go @@ -36,7 +36,7 @@ func (h *handler) showChooseSubscriptionPage(w http.ResponseWriter, r *http.Requ view.Set("menu", "feeds") view.Set("user", user) view.Set("countUnread", h.store.CountUnreadEntries(user.ID)) - view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID)) + view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID)) view.Set("defaultUserAgent", client.DefaultUserAgent) subscriptionForm := form.NewSubscriptionForm(r) diff --git a/ui/subscription_submit.go b/ui/subscription_submit.go index 44ca6f19..0eb11788 100644 --- a/ui/subscription_submit.go +++ b/ui/subscription_submit.go @@ -39,7 +39,7 @@ func (h *handler) submitSubscription(w http.ResponseWriter, r *http.Request) { v.Set("menu", "feeds") v.Set("user", user) v.Set("countUnread", h.store.CountUnreadEntries(user.ID)) - v.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID)) + v.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID)) v.Set("defaultUserAgent", client.DefaultUserAgent) v.Set("hasProxyConfigured", config.Opts.HasHTTPClientProxyConfigured()) @@ -102,7 +102,7 @@ func (h *handler) submitSubscription(w http.ResponseWriter, r *http.Request) { v.Set("menu", "feeds") v.Set("user", user) v.Set("countUnread", h.store.CountUnreadEntries(user.ID)) - v.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID)) + v.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID)) v.Set("hasProxyConfigured", config.Opts.HasHTTPClientProxyConfigured()) html.OK(w, r, v.Render("choose_subscription")) diff --git a/ui/unread_entries.go b/ui/unread_entries.go index e01c941f..57ff62b2 100644 --- a/ui/unread_entries.go +++ b/ui/unread_entries.go @@ -55,7 +55,7 @@ func (h *handler) showUnreadPage(w http.ResponseWriter, r *http.Request) { view.Set("menu", "unread") view.Set("user", user) view.Set("countUnread", countUnread) - view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID)) + view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID)) view.Set("hasSaveEntry", h.store.HasSaveEntry(user.ID)) html.OK(w, r, view.Render("unread_entries")) diff --git a/ui/user_create.go b/ui/user_create.go index c3269b22..a30f3346 100644 --- a/ui/user_create.go +++ b/ui/user_create.go @@ -33,7 +33,7 @@ func (h *handler) showCreateUserPage(w http.ResponseWriter, r *http.Request) { view.Set("menu", "settings") view.Set("user", user) view.Set("countUnread", h.store.CountUnreadEntries(user.ID)) - view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID)) + view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID)) html.OK(w, r, view.Render("create_user")) } diff --git a/ui/user_edit.go b/ui/user_edit.go index 3d0289ce..23f577db 100644 --- a/ui/user_edit.go +++ b/ui/user_edit.go @@ -52,7 +52,7 @@ func (h *handler) showEditUserPage(w http.ResponseWriter, r *http.Request) { view.Set("menu", "settings") view.Set("user", user) view.Set("countUnread", h.store.CountUnreadEntries(user.ID)) - view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID)) + view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID)) html.OK(w, r, view.Render("edit_user")) } diff --git a/ui/user_list.go b/ui/user_list.go index 37850c71..6c19de41 100644 --- a/ui/user_list.go +++ b/ui/user_list.go @@ -7,8 +7,8 @@ package ui // import "miniflux.app/ui" import ( "net/http" - "miniflux.app/http/response/html" "miniflux.app/http/request" + "miniflux.app/http/response/html" "miniflux.app/ui/session" "miniflux.app/ui/view" ) @@ -40,7 +40,7 @@ func (h *handler) showUsersPage(w http.ResponseWriter, r *http.Request) { view.Set("menu", "settings") view.Set("user", user) view.Set("countUnread", h.store.CountUnreadEntries(user.ID)) - view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID)) + view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID)) html.OK(w, r, view.Render("users")) } diff --git a/ui/user_save.go b/ui/user_save.go index f1f60584..01d26deb 100644 --- a/ui/user_save.go +++ b/ui/user_save.go @@ -2,13 +2,13 @@ // Use of this source code is governed by the Apache 2.0 // license that can be found in the LICENSE file. -package ui // import "miniflux.app/ui" +package ui // import "miniflux.app/ui" import ( "net/http" - "miniflux.app/http/response/html" "miniflux.app/http/request" + "miniflux.app/http/response/html" "miniflux.app/http/route" "miniflux.app/logger" "miniflux.app/ui/form" @@ -35,7 +35,7 @@ func (h *handler) saveUser(w http.ResponseWriter, r *http.Request) { view.Set("menu", "settings") view.Set("user", user) view.Set("countUnread", h.store.CountUnreadEntries(user.ID)) - view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID)) + view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID)) view.Set("form", userForm) if err := userForm.ValidateCreation(); err != nil { diff --git a/ui/user_update.go b/ui/user_update.go index 00b209d5..678b737e 100644 --- a/ui/user_update.go +++ b/ui/user_update.go @@ -47,7 +47,7 @@ func (h *handler) updateUser(w http.ResponseWriter, r *http.Request) { view.Set("menu", "settings") view.Set("user", user) view.Set("countUnread", h.store.CountUnreadEntries(user.ID)) - view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID)) + view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID)) view.Set("selected_user", selectedUser) view.Set("form", userForm) diff --git a/worker/worker.go b/worker/worker.go index f21f046a..0ca1eadb 100644 --- a/worker/worker.go +++ b/worker/worker.go @@ -5,7 +5,11 @@ package worker // import "miniflux.app/worker" import ( + "time" + + "miniflux.app/config" "miniflux.app/logger" + "miniflux.app/metric" "miniflux.app/model" "miniflux.app/reader/feed" ) @@ -22,11 +26,21 @@ func (w *Worker) Run(c chan model.Job) { for { job := <-c - logger.Debug("[Worker #%d] got userID=%d, feedID=%d", w.id, job.UserID, job.FeedID) + logger.Debug("[Worker #%d] Received feed #%d for user #%d", w.id, job.FeedID, job.UserID) - err := w.feedHandler.RefreshFeed(job.UserID, job.FeedID) - if err != nil { - logger.Error("[Worker] Feed #%d: %v", job.FeedID, err) + startTime := time.Now() + refreshErr := w.feedHandler.RefreshFeed(job.UserID, job.FeedID) + + if config.Opts.HasMetricsCollector() { + status := "success" + if refreshErr != nil { + status = "error" + } + metric.BackgroundFeedRefreshDuration.WithLabelValues(status).Observe(time.Since(startTime).Seconds()) + } + + if refreshErr != nil { + logger.Error("[Worker] Refreshing the feed #%d returned this error: %v", job.FeedID, refreshErr) } } }