From fe94d568408bd93ea591323fe7ff413c5c18aea8 Mon Sep 17 00:00:00 2001 From: Jelle van der Waa Date: Mon, 2 Mar 2020 10:54:52 +0100 Subject: [PATCH] Modularize the project and use golang project style Introduce a golang based project approach for the prometheus exporter. Create a seperate cmd with cli parsing by cobra, a server instance with logging using logrus. --- .gitignore | 9 +- .golangci.yaml | 74 +++++ .license-ranger.json | 4 + Dockerfile.build | 44 +++ Jenkinsfile | 51 +++ LICENSE.txt | 13 + Makefile | 119 +++++++ cmd/prometheus-kopano-exporter/log.go | 27 ++ .../prometheus-kopano-exporter/main.go | 5 +- cmd/prometheus-kopano-exporter/serve.go | 69 +++++ cmd/root.go | 171 +--------- go.mod | 11 + go.sum | 206 ++++++++++++ scripts/go-license-ranger.py | 222 +++++++++++++ scripts/prometheus-kopano-exporter.binscript | 47 +++ scripts/prometheus-kopano-exporter.cfg | 17 + scripts/prometheus-kopano-exporter.service | 20 ++ server/config.go | 18 ++ server/server.go | 293 ++++++++++++++++++ 19 files changed, 1254 insertions(+), 166 deletions(-) create mode 100644 .golangci.yaml create mode 100644 .license-ranger.json create mode 100644 Dockerfile.build create mode 100644 Jenkinsfile create mode 100644 LICENSE.txt create mode 100644 Makefile create mode 100644 cmd/prometheus-kopano-exporter/log.go rename main.go => cmd/prometheus-kopano-exporter/main.go (58%) create mode 100644 cmd/prometheus-kopano-exporter/serve.go create mode 100644 go.mod create mode 100644 go.sum create mode 100755 scripts/go-license-ranger.py create mode 100644 scripts/prometheus-kopano-exporter.binscript create mode 100644 scripts/prometheus-kopano-exporter.cfg create mode 100644 scripts/prometheus-kopano-exporter.service create mode 100644 server/config.go create mode 100644 server/server.go diff --git a/.gitignore b/.gitignore index 9fe65d9..f2be952 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,8 @@ -prometheus-kopano-exporter +/vendor +/bin +/golint.txt +/govet.txt +/dist +/test/tests.* +/3rdparty-LICENSES.md +/.vscode diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..f6917a9 --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,74 @@ +run: + modules-download-mode: vendor + +linters-settings: + govet: + check-shadowing: true + settings: + printf: + funcs: + - (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof + - (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf + - (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf + - (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf + golint: + min-confidence: 0 + gocyclo: + min-complexity: 10 + maligned: + suggest-new: true + dupl: + threshold: 100 + goconst: + min-len: 2 + min-occurrences: 2 + misspell: + locale: US + lll: + line-length: 140 + goimports: + local-prefixes: stash.kopano.io/kc/kapi + gocritic: + enabled-tags: + - performance + - style + - experimental + +linters: + # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint + disable-all: true + enable: + - bodyclose + - deadcode + - dupl + - errcheck + - funlen + - gochecknoinits + - goconst + - gocritic + - gocyclo + - gofmt + - goimports + - golint + - gosec + - gosimple + - govet + - ineffassign + - interfacer + - lll + - misspell + - nakedret + - scopelint + - staticcheck + - structcheck + - stylecheck + - typecheck + - unconvert + - unparam + - unused + - varcheck + + # don't enable: + # - depguard - until https://github.com/OpenPeeDeeP/depguard/issues/7 gets fixed + # - maligned,prealloc + # - gochecknoglobals diff --git a/.license-ranger.json b/.license-ranger.json new file mode 100644 index 0000000..afceb89 --- /dev/null +++ b/.license-ranger.json @@ -0,0 +1,4 @@ +{ + "mode": "mod-vendor", + "header": "# Kopano Prometheus Exporter 3rd party licenses\n\nCopyright 2020 Kopano and its licensors. See LICENSE.txt for license information. This document contains a list of open source components used in this project.\n\n## Kopano Prometheus Exporter prometheus-kopano-exporter\n" +} diff --git a/Dockerfile.build b/Dockerfile.build new file mode 100644 index 0000000..8912273 --- /dev/null +++ b/Dockerfile.build @@ -0,0 +1,44 @@ +# +# Copyright 2019 Kopano and its licensors +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 or +# later, as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +FROM golang:1.14.0-buster + +SHELL ["/bin/bash", "-o", "pipefail", "-c"] + +ARG GOLANGCI_LINT_TAG=v1.23.8 +RUN curl -sfL \ + https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | \ + sh -s -- -b /usr/local/bin ${GOLANGCI_LINT_TAG} + +RUN GOBIN=/usr/local/bin go get -v \ + github.com/tebeka/go2xunit \ + && go clean -cache && rm -rf /root/go + +ENV DEBIAN_FRONTEND noninteractive + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + build-essential \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /build + +ENV GOCACHE=/tmp/go-build +ENV GOPATH="" +ENV HOME=/tmp + +CMD ["make", "DATE=reproducible"] diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..5bb3786 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,51 @@ +#!/usr/bin/env groovy + +pipeline { + agent { + dockerfile { + filename 'Dockerfile.build' + } + } + stages { + stage('Bootstrap') { + steps { + echo 'Bootstrapping..' + sh 'go version' + } + } + stage('Lint') { + steps { + echo 'Linting..' + sh 'make lint-checkstyle' + checkstyle pattern: 'test/tests.lint.xml', canComputeNew: false, unstableTotalHigh: '100' + } + } + stage('Vendor') { + steps { + echo 'Fetching vendor dependencies..' + sh 'make vendor' + } + } + stage('Build') { + steps { + echo 'Building..' + sh 'make DATE=reproducible' + sh './bin/promtheus-kopano-exporter version && sha256sum ./bin/prometheus-kopano-exporter' + } + } + stage('Dist') { + steps { + echo 'Dist..' + sh 'test -z "$(git diff --shortstat 2>/dev/null |tail -n1)" && echo "Clean check passed."' + sh 'make check' + sh 'make dist' + } + } + } + post { + always { + archiveArtifacts 'dist/*.tar.gz' + cleanWs() + } + } +} diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..1272dd9 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,13 @@ +Copyright 2020 Kopano and its licensors + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License, version 3, +as published by the Free Software Foundation. + +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see http://www.gnu.org/licenses/. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d5e7adf --- /dev/null +++ b/Makefile @@ -0,0 +1,119 @@ +PACKAGE = stash.kopano.io/kgol/prometheus-kopano-exporter +PACKAGE_NAME = prometheus-kopano-exporter + +# Tools + +GO ?= go +GOFMT ?= gofmt +GOLINT ?= golangci-lint + +# Cgo + +CGO_ENABLED ?= 0 + +# Go modules + +GO111MODULE ?= on + +# Variables + +export CGO_ENABLED GO111MODULE +unexport GOPATH + +ARGS ?= +PWD := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) +DATE ?= $(shell date -u +"%Y-%m-%dT%H:%M:%SZ") +VERSION ?= $(shell git describe --tags --always --dirty --match=v* 2>/dev/null | sed 's/^v//' || \ + cat $(CURDIR)/.version 2> /dev/null || echo 0.0.0-unreleased) +PKGS = $(or $(PKG),$(shell $(GO) list -mod=readonly ./... | grep -v "^$(PACKAGE)/vendor/")) +TESTPKGS = $(shell $(GO) list -mod=readonly -f '{{ if or .TestGoFiles .XTestGoFiles }}{{ .ImportPath }}{{ end }}' $(PKGS) 2>/dev/null) +CMDS = $(or $(CMD),$(addprefix cmd/,$(notdir $(shell find "$(PWD)/cmd/" -type d)))) +TIMEOUT = 30 + +# Build + +.PHONY: all +all: fmt | $(CMDS) $(PLUGINS) + +plugins: fmt | $(PLUGINS) + +.PHONY: $(CMDS) +$(CMDS): vendor ; $(info building $@ ...) @ + CGO_ENABLED=$(CGO_ENABLED) $(GO) build \ + -mod=vendor \ + -trimpath \ + -tags release \ + -buildmode=exe \ + -ldflags '-s -w -buildid=reproducible/$(VERSION) -X $(PACKAGE)/version.Version=$(VERSION) -X $(PACKAGE)/version.BuildDate=$(DATE) -extldflags -static' \ + -o bin/$(notdir $@) ./$@ + +# Helpers + +.PHONY: lint +lint: vendor ; $(info running $(GOLINT) ...) @ + $(GOLINT) run + +.PHONY: lint-checkstyle +lint-checkstyle: vendor ; $(info running $(GOLINT) checkstyle ...) @ + @mkdir -p test + $(GOLINT) run --out-format checkstyle --issues-exit-code 0 > test/tests.lint.xml + +.PHONY: fmt +fmt: ; $(info running gofmt ...) @ + @ret=0 && for d in $$($(GO) list -mod=readonly -f '{{.Dir}}' ./... | grep -v /vendor/); do \ + $(GOFMT) -l -w $$d/*.go || ret=$$? ; \ + done ; exit $$ret + +.PHONY: check +check: ; $(info checking dependencies ...) @ + @$(GO) mod verify && echo OK + +# Mod + +go.sum: go.mod ; $(info updating dependencies ...) + @$(GO) mod tidy -v + @touch $@ + +.PHONY: vendor +vendor: go.sum ; $(info retrieving dependencies ...) + @$(GO) mod vendor -v + @touch $@ + +# Dist + +.PHONY: licenses +licenses: vendor ; $(info building licenses files ...) + $(CURDIR)/scripts/go-license-ranger.py > $(CURDIR)/3rdparty-LICENSES.md + +3rdparty-LICENSES.md: licenses + +.PHONY: dist +dist: 3rdparty-LICENSES.md ; $(info building dist tarball ...) + @rm -rf "dist/${PACKAGE_NAME}-${VERSION}" + @mkdir -p "dist/${PACKAGE_NAME}-${VERSION}" + @mkdir -p "dist/${PACKAGE_NAME}-${VERSION}/scripts" + @cd dist && \ + cp -avf ../LICENSE.txt "${PACKAGE_NAME}-${VERSION}" && \ + cp -avf ../README.md "${PACKAGE_NAME}-${VERSION}" && \ + cp -avf ../3rdparty-LICENSES.md "${PACKAGE_NAME}-${VERSION}" && \ + cp -avf ../bin/* "${PACKAGE_NAME}-${VERSION}" && \ + cp -avf ../scripts/prometheus-kopano-exporter.binscript "${PACKAGE_NAME}-${VERSION}/scripts" && \ + cp -avf ../scripts/prometheus-kopano-exporter.service "${PACKAGE_NAME}-${VERSION}/scripts" && \ + cp -avf ../scripts/prometheus-kopano-exporter.cfg "${PACKAGE_NAME}-${VERSION}/scripts" && \ + tar --owner=0 --group=0 -czvf ${PACKAGE_NAME}-${VERSION}.tar.gz "${PACKAGE_NAME}-${VERSION}" && \ + cd .. + +.PHONE: changelog +changelog: ; $(info updating changelog ...) + $(CHGLOG) --output CHANGELOG.md $(ARGS) + +# Rest + +.PHONY: clean +clean: ; $(info cleaning ...) @ + @rm -rf bin + @rm -rf test/test.* + +.PHONY: version +version: + @echo $(VERSION) diff --git a/cmd/prometheus-kopano-exporter/log.go b/cmd/prometheus-kopano-exporter/log.go new file mode 100644 index 0000000..409c97b --- /dev/null +++ b/cmd/prometheus-kopano-exporter/log.go @@ -0,0 +1,27 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-or-later + * Copyright 2020 Kopano and its licensors + */ + +package main + +import ( + "os" + + "github.com/sirupsen/logrus" +) + +func newLogger(disableTimestamp bool, logLevelString string) (logrus.FieldLogger, error) { + logLevel, err := logrus.ParseLevel(logLevelString) + if err != nil { + return nil, err + } + + return &logrus.Logger{ + Out: os.Stderr, + Formatter: &logrus.TextFormatter{ + DisableTimestamp: disableTimestamp, + }, + Level: logLevel, + }, nil +} diff --git a/main.go b/cmd/prometheus-kopano-exporter/main.go similarity index 58% rename from main.go rename to cmd/prometheus-kopano-exporter/main.go index 01d6144..d032f41 100644 --- a/main.go +++ b/cmd/prometheus-kopano-exporter/main.go @@ -1,6 +1,6 @@ /* * SPDX-License-Identifier: AGPL-3.0-or-later - * Copyright 2019 Kopano and its licensors + * Copyright 2020 Kopano and its licensors */ package main @@ -10,5 +10,6 @@ import ( ) func main() { - cmd.Execute() + cmd.RootCmd.AddCommand(commandServe()) + cmd.RootCmd.Execute() } diff --git a/cmd/prometheus-kopano-exporter/serve.go b/cmd/prometheus-kopano-exporter/serve.go new file mode 100644 index 0000000..6fb83e2 --- /dev/null +++ b/cmd/prometheus-kopano-exporter/serve.go @@ -0,0 +1,69 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-or-later + * Copyright 2020 Kopano and its licensors + */ + +package main + +import ( + "context" + "fmt" + "os" + + "github.com/spf13/cobra" + "stash.kopano.com/kgol/prometheus-kopano-exporter/server" +) + +var ( + listensocket = "/run/prometheus-kopano-exporter/exporter.sock" + listenaddress = "localhost:6231" +) + +func commandServe() *cobra.Command { + serveCmd := &cobra.Command{ + Use: "serve", + Short: "Prometheus exporter for Kopano server, dagent and spooler", + Run: func(cmd *cobra.Command, args []string) { + if err := serve(cmd, args); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } + }, + } + + serveCmd.Flags().StringVar(&listensocket, "listen-socket", listensocket, "Custom socket location") + serveCmd.Flags().StringVar(&listenaddress, "listen-address", listenaddress, "Address on which to expose metrics and web interface.") + + serveCmd.Flags().Bool("log-timestamp", true, "Prefix each log line with timestamp") + serveCmd.Flags().String("log-level", "info", "Log level (one of panic, fatal, error, warn, info or debug)") + + return serveCmd +} + +func serve(cmd *cobra.Command, args []string) error { + ctx := context.Background() + + logTimestamp, _ := cmd.Flags().GetBool("log-timestamp") + logLevel, _ := cmd.Flags().GetString("log-level") + + logger, err := newLogger(!logTimestamp, logLevel) + if err != nil { + return fmt.Errorf("failed to create logger: %w", err) + } + + logger.Debugln("starting kopano prometheus exporter") + + cfg := &server.Config{ + ListenSocket: listensocket, + ListenAddress: listenaddress, + + Logger: logger, + } + + srv, err := server.NewServer(cfg) + if err != nil { + return err + } + + return srv.Serve(ctx) +} diff --git a/cmd/root.go b/cmd/root.go index b2fe217..919d2c7 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,175 +1,20 @@ /* * SPDX-License-Identifier: AGPL-3.0-or-later - * Copyright 2019 Kopano and its licensors + * Copyright 2020 Kopano and its licensors */ + package cmd import ( - "fmt" - "encoding/json" - "net" - "net/http" - "io/ioutil" - "log" "os" - "sync" - "strings" - - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promauto" - "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/spf13/cobra" ) -const ( - defaultBadRequestMessage = "Invalid data received." - defaultTeaPotMessage = "Sorry - you are a teapot." - defaultServerErrorMessage = "Sorry - something bad has happend." -) - -var ( - collectormap sync.Map - socketloc = "/tmp/kopano-prometheus.sock" - httpport = ":9099" -) - -var rootCmd = &cobra.Command{ - Use: "prometheus-kopano-exporter", - Short: "Prometheus exporter for Kopano server, dagent and spooler", - Run: func(cmd *cobra.Command, args []string) { - // Do Stuff Here - fmt.Println("starting kopano prometheus exporter") - - - // Start promethes metrics - go func() { - http.Handle("/metrics", promhttp.Handler()) - log.Fatal(http.ListenAndServe(httpport, nil)) - }() - - if err := os.RemoveAll(socketloc); err != nil { - log.Fatal(err) - } - - // TODO: add healthcheck for docker - http.HandleFunc("/", collectMetricsHandler) - - unixListener, err := net.Listen("unix", socketloc) - if err != nil { - panic(err) - } - - defer unixListener.Close() - - log.Fatal(http.Serve(unixListener, nil)) - }, -} - -func WriteBadRequestPage(rw http.ResponseWriter, message string) { - if message == "" { - message = defaultBadRequestMessage - } - http.Error(rw, message, http.StatusBadRequest) -} - -func collectMetricsHandler(rw http.ResponseWriter, req *http.Request) { - if req.Header.Get("X-Kopano-Stats-Request") != "1" { - WriteBadRequestPage(rw, "missing header") - return - } - - if req.Method != http.MethodPost { - WriteBadRequestPage(rw, "not a post") - return - } - - if !strings.HasPrefix(req.Header.Get("Content-Type"), "application/json") { - WriteBadRequestPage(rw, "json content type") - return - } - - body, err := ioutil.ReadAll(http.MaxBytesReader(rw, req.Body, 1*1024*1024)) - if err != nil { - // TODO: logger - fmt.Fprintln(rw, "failed to read client data request") - WriteBadRequestPage(rw, "") - return - } - - // Parse JSON. - payload := make(map[string]interface{}) - if err := json.Unmarshal(body, &payload); err != nil { - fmt.Fprintln(rw, "failed to parse json") - WriteBadRequestPage(rw, "") - return - } - - // Iterate over stats and add counters - stats, ok := payload["stats"].(map[string]interface{}) - if !ok { - WriteBadRequestPage(rw, "") - return - } - - program_key := stats["program_name"].(map[string]interface{}) - program_name := strings.ReplaceAll(program_key["value"].(string), "-", "_") - - // Some metrics are prefixed with the program name such as 'dagent_deliver_junk' - strip_string := "" - if strings.HasPrefix(program_name, "kopano_") { - // Use split? - strip_string = strings.Replace(program_name, "kopano_", "", 1) - strip_string += "_" - } - - log.Printf("Receiving metrics from %s", program_name) - - for key, value := range stats { - val, _ := value.(map[string]interface{}) - - if strings.HasPrefix(key, strip_string) { - key = strings.Replace(key, strip_string, "", 1) - } - - key = program_name + "_" + key - - switch val["mode"] { - case "gauge", "counter": - metrictype := val["type"] - if metrictype != "int" { - log.Printf("skipping metric '%s' as it's not of type integer", key) - break - } - - gauge, ok := collectormap.Load(key) - if ! ok { - gauge = promauto.NewGauge(prometheus.GaugeOpts{ - Name: key, - Help: val["desc"].(string), - }) - collectormap.Store(key, gauge) - } - - gauge.(prometheus.Gauge).Set(val["value"].(float64)) - break - case "unixtime": - break - default: - log.Printf("metric '%s' without mode", key) - } - } - - // TODO: Validate JSON schema. - fmt.Fprintf(rw, "collected") -} - -func Execute() { - rootCmd.Flags().StringVar(&socketloc, "socket-loc", socketloc, "Custom socket location") - rootCmd.Flags().StringVar(&httpport, "listen-address", httpport, "Address on which to expose metrics and web interface.") - - if err := rootCmd.Execute(); err != nil { - fmt.Println(err) - os.Exit(1) - } +// RootCmd provides the commandline parser root. +var RootCmd = &cobra.Command{ + Run: func(cmd *cobra.Command, args []string) { + cmd.Help() + os.Exit(2) + }, } diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..78d2a8d --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module stash.kopano.com/kgol/prometheus-kopano-exporter + +go 1.13 + +require ( + github.com/prometheus/client_golang v1.6.0 + github.com/sirupsen/logrus v1.6.0 + github.com/spf13/cobra v1.0.0 + github.com/spf13/pflag v1.0.5 // indirect + golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..0124177 --- /dev/null +++ b/go.sum @@ -0,0 +1,206 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +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/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +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/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +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/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +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/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +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-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-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/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-20190129154638-5b532d6fd5ef/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/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 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +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/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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/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.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +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 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= +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 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= +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 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +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 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +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/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +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/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.6.0 h1:YVPodQOcK15POxhgARIvnDRVpLcuK8mglnMrWfyrw6A= +github.com/prometheus/client_golang v1.6.0/go.mod h1:ZLOG9ck3JLRdB5MgO8f+lLTe83AXG6ro35rLTxvnIl4= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/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.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.9.1 h1:KOMtN28tlbam3/7ZKEYKHhKoJZYYj3gMH4uc62x7X7U= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.11 h1:DhHlBtkHWPYi8O2y31JkK0TF+DGM+51OopZjH/Ia5qI= +github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +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 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8= +github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +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 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/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-20181220203305-927f97764cc3/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-20190522155817-f3200d17e092/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/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +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/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-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-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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f h1:gWF768j/LaZugp8dyS4UwsslYCYz9XgFxvlgsn0n9H8= +golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3 h1:5B6i6EAiSYyejWfvc5Rc9BbI3rzIsrrXfAQBWnYfn+w= +golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/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-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +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/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +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= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +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 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +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 h1:ymVxjfMaHvXD8RqPRmzHHsB3VvucivSkIAvJFDI5O3c= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/scripts/go-license-ranger.py b/scripts/go-license-ranger.py new file mode 100755 index 0000000..5e1d08e --- /dev/null +++ b/scripts/go-license-ranger.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python +# +# go-license-ranger. A simple script to generate a 3rd party license file out o +# a Go dependency tree. Requires [Go modules](https://github.com/golang/go/wiki/Modules), +# [glide](https://glide.sh) or [dep](https://golang.github.io/dep/) to find the +# dependencies. +# +# +# Copyright 2018-2020 Kopano and its licensors +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +from __future__ import print_function + +import json +import os +from os import listdir +from os.path import abspath, isdir, join +import subprocess +import sys + +version = "20200309-1" + +# Default configuration. Override possible with `.license-ranger.json` or with +# a custom name if environment variable is set to a different value. +defaultConfigFilename = os.environ.get("LICENSE_RANGER_CONFIG", + ".license-ranger.json") +config = { + "allowMissing": 0, + "base": "./vendor", + "debug": os.environ.get("DEBUG", None) == "1", + "footer": "", + "glide": os.environ.get("GLIDE", "glide"), + "dep": os.environ.get("DEP", "dep"), + "go": os.environ.get("GO", "go"), + "mode": os.environ.get("LICENSE_RANGER_MODE", "mod"), + "header": "", + "licenseFilenames": [ + 'LICENSE', + 'LICENSE.md', + 'LICENSE.txt', + 'COPYING', + 'license', + 'license.md', + 'license.txt' + ], + # "manual": { + # "github.com/kopano-dev/example": "README.txt" + # } +} + +# Main below. + + +def main(): + loadConfigFromFile() + if config.get("mode", None) == "mod": + dependencyFolders = getDependenciesWithMod() + elif config.get("mode", None) == "mod-vendor": + dependencyFolders = getDependenciesWithModVendor() + elif config.get("mode", None) == "dep": + dependencyFolders = getDependenciesWithDep(config["dep"]) + elif config.get("mode", None) == "glide": + dependencyFolders = getDependenciesWithGlide(config["glide"]) + else: + print("Error: Invalid mode %s", config.get("mode")) + sys.exit(1) + dependencyFolders.sort() + missing = run(config["base"], dependencyFolders, + config["licenseFilenames"]) + if len(missing) > config["allowMissing"]: + print("Failed: Missing licenses: %d" % len(missing), file=sys.stderr) + for m in missing: + print("> %s" % m, file=sys.stderr) + sys.exit(1) + + +def loadConfigFromFile(configFilename=defaultConfigFilename): + try: + with open(configFilename, "r") as configFile: + loadedConfig = json.loads(configFile.read()) + for k, v in loadedConfig.items(): + config[k] = v + except FileNotFoundError: + pass + + +def getDependenciesWithModVendor(): + installed = [] + with open(os.path.join("vendor", "modules.txt"), "r") as f: + for line in f.readlines(): + line = line.strip() + if line and not line.startswith("#"): + name = line.split(" ", 1)[0].strip() + if name: + installed.append(name) + return installed + +def getDependenciesWithMod(): + installed = [] + with open("go.sum", "r") as f: + for line in f.readlines(): + line = line.strip() + if line: + name = line.split(" ", 1)[0].strip() + if name: + installed.append(name) + return installed + + +def getDependenciesWithGlide(glide="glide"): + result = subprocess.check_output([glide, 'list', '-o', 'json']) + data = json.loads(result.decode('utf-8')) + if data.get("missing", []): + raise ValueError("missing dependencies are not allowed: %s" % + data.get("missing").join(",")) + return data.get("installed", []) + + +def getDependenciesWithDep(dep="dep"): + result = subprocess.check_output([dep, 'status', '-json']) + data = json.loads(result.decode('utf-8')) + installed = [] + for d in data: + installed.append(d.get("ProjectRoot")) + return installed + + +def run(base, relativeFolderPaths, licenseFileNames): + relativeFolderPaths.sort() + missing, table = getLicenseTable(base, relativeFolderPaths, + licenseFileNames) + concatLicenses(table) + return missing + + +def getLicenseTable(base, relativeFolderPaths, licenseFileNames): + table = {} + missing = [] + + for folder in relativeFolderPaths: + folderPath = join(base, folder) + if not isdir(folderPath): + continue + licenses = findLicenseFile(base, table, abspath(folderPath), + licenseFileNames) + + if len(licenses) == 0 and folder in config.get("manual", ()): + if config["debug"]: + print("> Using manual license definition: %s" % folderPath, + file=sys.stderr) + licenses = [join(folderPath, config.get("manual")[folder])] + + if len(licenses) > 0: + table[folderPath] = (folder, licenses) + continue + + if config["debug"]: + print("> Missing LICENSE: %s" % folderPath, file=sys.stderr) + missing.append(folder) + + if config["debug"]: + print("> Total missing: %d" % len(missing), file=sys.stderr) + + return missing, table + + +def findLicenseFile(base, table, folderPath, licenseFileNames): + result = [] + for f in listdir(folderPath): + if f not in licenseFileNames: + continue + result.append(join(folderPath, f)) + if len(result) == 0: + parentFolderPath = abspath(join(folderPath, "..")) + if parentFolderPath in table: + return table[parentFolderPath] + else: + if parentFolderPath != abspath(base): + return findLicenseFile(base, table, parentFolderPath, + licenseFileNames) + return result + + +def concatLicenses(table): + seen = {} + tableValues = list(table.values()) + tableValues.sort() + + print(config.get("header", "")) + for v in tableValues: + module, licenses = v + for license in licenses: + if license in seen: + if config["debug"]: + print("> Double %s:%s" % (module, license), + file=sys.stderr) + continue + seen[license] = True + if config["debug"]: + print("> Licens %s" % license, file=sys.stderr) + + print("### %s\n" % module) + with open(license, 'r') as licenseFile: + print("```\n%s\n```\n" % licenseFile.read()) + print(config.get("footer", "")) + + +if __name__ == "__main__": + main() diff --git a/scripts/prometheus-kopano-exporter.binscript b/scripts/prometheus-kopano-exporter.binscript new file mode 100644 index 0000000..afc046c --- /dev/null +++ b/scripts/prometheus-kopano-exporter.binscript @@ -0,0 +1,47 @@ +#!/bin/sh +# +# Kopano Prometheus Exporter (prometheus-kopano-exporter) launcher +# +# License: Apache-2.0 +# Copyright 2020 Kopano and its licensors +# + +set -e + +# Base defines. + +EXE=/usr/libexec/kopano/prometheus-kopano-exporter + +# Handle parameters for configuration. + +case "${1}" in + serve) + # Inject values from environment into command line. This is mainly used + # when this script is run from systemd or docker. + + shift + + if [ -n "$listen" ]; then + set -- "$@" --listen-address="$listen" + fi + + if [ -n "$socket" ]; then + set -- "$@" --listen-socket="$socket" + fi + + if [ -n "$log_level" ]; then + set -- "$@" --log-level="$log_level" + fi + + ;; + *) + ;; +esac + +# Set executable. + +set -- ${EXE} "$@" + +# Run. + +exec "$@" diff --git a/scripts/prometheus-kopano-exporter.cfg b/scripts/prometheus-kopano-exporter.cfg new file mode 100644 index 0000000..6793706 --- /dev/null +++ b/scripts/prometheus-kopano-exporter.cfg @@ -0,0 +1,17 @@ +############################################################## +# Kopano Prometheus Exporter SETTINGS + +# Address:port specifier for where prometheus exporter should listen for +# incoming connections. Defaults to `127.0.0.1:6231`. +#listen = 127.0.0.1:6231 + +# The unix socket where the Kopano Dagent, Server and Spooler send their data. +# Defaults to `/run/prometheus-kopano-exporter/exporter.sock` and is only used +#socket = /run/prometheus-kopano-exporter/exporter.sock + +############################################################### +# Log settings + +# Log level controls the verbosity of the output log. It can be one of +# `panic`, `fatal`, `error`, `warn`, `info` or `debug`. Defaults to `info`. +#log_level = info diff --git a/scripts/prometheus-kopano-exporter.service b/scripts/prometheus-kopano-exporter.service new file mode 100644 index 0000000..ed7ad2f --- /dev/null +++ b/scripts/prometheus-kopano-exporter.service @@ -0,0 +1,20 @@ +[Unit] +Description=Kopano Prometheus Exporter Daemon + +[Service] +Type=simple +PrivateTmp=yes +User=kopanoexporter +Group=kopano +NoNewPrivileges=yes +PrivateUsers=yes +CapabilityBoundingSet= +ProtectSystem=full +UMask=0077 +PermissionsStartOnly=true +Environment=LC_CTYPE=en_US.UTF-8 +EnvironmentFile=-/etc/kopano/prometheus-kopano-exporter.cfg +ExecStart=/usr/sbin/prometheus-kopano-exporter serve --log-timestamp=false + +[Install] +WantedBy=multi-user.target diff --git a/server/config.go b/server/config.go new file mode 100644 index 0000000..293a5ab --- /dev/null +++ b/server/config.go @@ -0,0 +1,18 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-or-later + * Copyright 2020 Kopano and its licensors + */ + +package server + +import ( + "github.com/sirupsen/logrus" +) + +// Config bundles configuration settings. +type Config struct { + ListenSocket string + ListenAddress string + + Logger logrus.FieldLogger +} diff --git a/server/server.go b/server/server.go new file mode 100644 index 0000000..804ea76 --- /dev/null +++ b/server/server.go @@ -0,0 +1,293 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-or-later + * Copyright 2020 Kopano and its licensors + */ +package server + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net" + "net/http" + "os" + "os/signal" + "strings" + "sync" + "syscall" + "time" + + "github.com/sirupsen/logrus" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +const ( + defaultBadRequestMessage = "Invalid data received." + defaultTeaPotMessage = "Sorry - you are a teapot." + defaultServerErrorMessage = "Sorry - something bad has happend." +) + +var ( + collectormap sync.Map +) + +// Server is our HTTP server implementation. +type Server struct { + config *Config + + logger logrus.FieldLogger +} + +func NewServer(c *Config) (*Server, error) { + s := &Server{ + config: c, + logger: c.Logger, + } + + return s, nil +} + +func (s *Server) Serve(ctx context.Context) error { + var err error + var wg sync.WaitGroup + + _, serveCtxCancel := context.WithCancel(ctx) + defer serveCtxCancel() + + logger := s.logger + + if err := os.RemoveAll(s.config.ListenSocket); err != nil { + logger.WithError(err).Errorf("failed to remove socket location") + return err + } + + unixListener, err := net.Listen("unix", s.config.ListenSocket) + if err != nil { + logger.WithError(err).Errorf("failed to create socket") + return err + } + defer unixListener.Close() + + listener, err := net.Listen("tcp", s.config.ListenAddress) + if err != nil { + logger.WithError(err).Errorf("failed to create http socket") + return err + } + defer listener.Close() + + promServer := http.Server{ + Handler: promhttp.Handler(), + } + + unixHandler := http.HandlerFunc(s.collectMetricsHandler) + unixServer := http.Server{ + Handler: unixHandler, + } + + errCh := make(chan error, 2) + exitCh := make(chan bool, 1) + signalCh := make(chan os.Signal, 1) + + wg.Add(1) + + // Start Prometheus metrics + go func() { + defer wg.Done() + + err := promServer.Serve(listener) + if err != nil { + errCh <- err + } + + logger.Debugln("http listener stopped") + }() + + wg.Add(1) + + // Start metrics endpoint + go func() { + defer wg.Done() + + err = unixServer.Serve(unixListener) + if err != nil { + errCh <- err + } + + logger.Debugln("unix listener stopped") + }() + + // Wait for exit or error. + signal.Notify(signalCh, syscall.SIGINT, syscall.SIGTERM) + select { + case err = <-errCh: + // breaks + case reason := <-signalCh: + logger.WithField("signal", reason).Warnln("received signal") + // breaks + } + + logger.Infoln("clean server shutdown start") + + shutDownCtx, shutDownCtxCancel := context.WithTimeout(ctx, 10*time.Second) + go func() { + if shutdownErr := promServer.Shutdown(shutDownCtx); shutdownErr != nil { + logger.WithError(shutdownErr).Warn("clean http server shutdown failed") + } + }() + + shutDownCtx2, shutDownCtxCancel2 := context.WithTimeout(ctx, 10*time.Second) + go func() { + if shutdownErr := unixServer.Shutdown(shutDownCtx2); shutdownErr != nil { + logger.WithError(shutdownErr).Warn("clean unix server shutdown failed") + } + }() + + go func() { + wg.Wait() + close(exitCh) + }() + shutDownCtxCancel() // prevent leak. + shutDownCtxCancel2() // prevent leak. + + // Cancel our own context, + serveCtxCancel() + func() { + for { + select { + case <-exitCh: + return + default: + // Unix/HTTP listener has not quit yet. + logger.Info("waiting for listeners to exit") + } + select { + case reason := <-signalCh: + logger.WithField("signal", reason).Warn("received signal") + return + case <-time.After(100 * time.Millisecond): + } + } + }() + + if errors.Is(err, http.ErrServerClosed) { + err = nil + } + + return err +} + +func (s *Server) collectMetricsHandler(rw http.ResponseWriter, req *http.Request) { + logger := s.logger + + if req.Header.Get("X-Kopano-Stats-Request") != "1" { + WriteBadRequestPage(rw, "missing header") + return + } + + if req.Method != http.MethodPost { + WriteBadRequestPage(rw, "request must use the POST method") + return + } + + if !strings.HasPrefix(req.Header.Get("Content-Type"), "application/json") { + WriteBadRequestPage(rw, "json content type") + return + } + + body, err := ioutil.ReadAll(http.MaxBytesReader(rw, req.Body, 1*1024*1024)) + if err != nil { + logger.WithError(err).Errorf("failed to read client data request") + fmt.Fprintln(rw, "failed to read client data request") + WriteBadRequestPage(rw, "") + return + } + + // Parse JSON. + payload := make(map[string]interface{}) + if err := json.Unmarshal(body, &payload); err != nil { + fmt.Fprintln(rw, "failed to parse json") + logger.WithError(err).Errorf("failed to parse JSON") + WriteBadRequestPage(rw, "") + return + } + + // Iterate over stats and add counters + stats, ok := payload["stats"].(map[string]interface{}) + if !ok { + WriteBadRequestPage(rw, "") + return + } + + program_key := stats["program_name"].(map[string]interface{}) + program_name := strings.ReplaceAll(program_key["value"].(string), "-", "_") + + // Some metrics are prefixed with the program name such as 'dagent_deliver_junk' + strip_string := "" + if strings.HasPrefix(program_name, "kopano_") { + // Use split? + strip_string = strings.Replace(program_name, "kopano_", "", 1) + strip_string += "_" + } + + logger.WithField("program", program_name).Info("receiving metrics") + + for key, value := range stats { + val, _ := value.(map[string]interface{}) + + if strings.HasPrefix(key, strip_string) { + key = strings.Replace(key, strip_string, "", 1) + } + + key = program_name + "_" + key + mode := val["mode"] + + switch mode { + case "gauge", "counter": + metrictype := val["type"] + if metrictype != "int" { + logger.WithField("metrictype", key).Debug("skipping non integer metric type") + break + } + + gauge, ok := collectormap.Load(key) + if !ok { + gauge = promauto.NewGauge(prometheus.GaugeOpts{ + Name: key, + Help: val["desc"].(string), + }) + collectormap.Store(key, gauge) + } + + gauge.(prometheus.Gauge).Set(val["value"].(float64)) + break + case "unixtime": + break + default: + if mode != nil { + logger.WithField("mode", mode).Debug("unsupported metric mode") + } else { + logger.WithField("key", key).Debug("metric has no mode") + } + } + } + + // TODO: Validate JSON schema. + fmt.Fprintf(rw, "collected") +} + +// TODO: add a more useful health report +func (s *Server) healthCheckHandler(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(http.StatusOK) +} + +func WriteBadRequestPage(rw http.ResponseWriter, message string) { + if message == "" { + message = defaultBadRequestMessage + } + http.Error(rw, message, http.StatusBadRequest) +}