diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000000000000000000000000000000000000..c2f20be1fec828bcecf0ac9ea324332d9bfe194b --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,49 @@ +--- +version: 2.1 + +executors: + # Whenever the Go version is updated here, .travis.yml and .promu.yml + # should also be updated. + golang: + docker: + - image: circleci/golang:1.12 + +jobs: + test: + executor: golang + + steps: + - prometheus/setup_environment + - run: make + - prometheus/store_artifact: + file: prometheus-gitlab-notifier + +workflows: + version: 2 + pushgateway: + jobs: + - test: + filters: + tags: + only: /.*/ + - prometheus/build: + name: build + filters: + tags: + only: /.*/ + - prometheus/publish_master: + requires: + - test + - build + filters: + branches: + only: master + - prometheus/publish_release: + requires: + - test + - build + filters: + tags: + only: /^v[0-9]+(\.[0-9]+){2}(-.+|[^-.]*)$/ + branches: + ignore: /.*/ diff --git a/.gitignore b/.gitignore index f1c181ec9c5c921245027c6b452ecfc1d3626364..2336c7440c14e3b07d9d9a659445c2aa1a09dfe4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +# Created by .ignore support plugin (hsz.mobi) +### Go template # Binaries for programs and plugins *.exe *.exe~ @@ -5,8 +7,15 @@ *.so *.dylib -# Test binary, build with `go test -c` +# Test binary, built with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +/conf/*.token +/prometheus-gitlab-notifier + diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000000000000000000000000000000000000..f9abe8d6ffe60de80fce45af93b4ab8188d9ef3e --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,6 @@ +service: + golangci-lint-version: 1.12.4 + +linters-settings: + errcheck: + exclude: errcheck_excludes.txt diff --git a/.promu.yml b/.promu.yml new file mode 100644 index 0000000000000000000000000000000000000000..d4e4510a412987224850ba3de231abf637cbca96 --- /dev/null +++ b/.promu.yml @@ -0,0 +1,20 @@ +go: + # Whenever the Go version is updated here, .travis.yml and + # .circle/config.yml should also be updated. + version: 1.12 +repository: + path: github.com/fusakla/prometheus-gitlab-notifier +build: + binaries: + - name: prometheus-gitlab-notifier + path: ./cmd/prometheus-gitlab-notifier + flags: -a -tags netgo + ldflags: | + -X github.com/fusakla/prometheus-gitlab-notifier/metrics.appVersion={{.Version}} + -X github.com/fusakla/prometheus-gitlab-notifier/metrics.gitRevision={{.Revision}} + -X github.com/fusakla/prometheus-gitlab-notifier/metrics.gitBranch={{.Branch}} + -X github.com/fusakla/prometheus-gitlab-notifier/metrics.gitTag={{.Version}} +tarball: + files: + - LICENSE + - NOTICE diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000000000000000000000000000000000..cf94cc5870416ab8f584847f7099b8646eba352b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,62 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.6.0] - 2019-07-17 + +>**! Warning:** This release significantly changes logic of creating Gitlab issues and labeling scheme. +Please read more about the new grouping feature. + +## Changed +- Dynamic labels are now added as scoped labels to the issues in form `label::value` +- To every issue the group- To every issue the grouping labels are added as scoped labels same way as dynamic labels. +ing labels are added as scoped labels same way as dynamic labels. + +## Added +- If alert comes and opened issue with the same group labels is present in the Gitlab, +the rendered template is just appended to this already existing issue instead of creating a new one. +This applies only for issues younger than by default `1h` which can be controlled by new flag `--group.interval`. +Every appended issue gets new scoped label `appended-alerts::<numer>` with number of times it was appended. +- Readme notes about contributing and release. + +## [0.5.0] - 2019-07-10 + +## Added +- Added dynamic label addition from the alert labels using flag `dynamic.issue.label.name` + +## [0.4.1] - 2019-06-27 + +## Fixed +- Metric `app_build_info` is now initialized to value `1` + +## [0.4.0] - 2019-06-27 + +## Added +- Added time to log messages +- Added metric `app_build_info` with info about version of the app, build etc. + +## [0.3.0] - 2019-06-26 + +## Changed +- Removed Gitlab call from readiness probe since the alerts +are just enqueued and retrying should take care of that. + +## Added +- Check on startup that Gitlab is reachable. + +## [0.2.0] - 2019-06-26 + +## Added: +- Added `status_code` to metrics and access log. + +## Changed +- Refactored HTTP server middleware. + +## [0.1.0] - 2019-06-25 + +Initial release + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..84792d8aaf8c17c676665fc2caa7e841c1e259ac --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +ARG ARCH="amd64" +ARG OS="linux" +FROM quay.io/prometheus/busybox:latest +LABEL maintainer="FUSAKLA Martin Chodúr <m.chodur@seznam.cz>" + +COPY --chown=nobody:nogroup .build/${OS}-${ARCH}/prometheus-gitlab-notifier /bin/prometheus-gitlab-notifier +COPY --chown=nobody:nogroup conf/default_issue.tmpl /prometheus-gitlab-notifier/conf/ +COPY --chown=nobody:nogroup Dockerfile / + +EXPOSE 9288 +RUN mkdir -p /prometheus-gitlab-notifier && chown nobody:nogroup /prometheus-gitlab-notifier +WORKDIR /prometheus-gitlab-notifier + +USER 65534 + +ENTRYPOINT ["/bin/prometheus-gitlab-notifier"] diff --git a/LICENSE b/LICENSE index 43fddfdf38cdc58c8760481abd2fe9d688e22806..261eeb9e9f8b2b4b0d119366dda99c6fd7d35c64 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,201 @@ -MIT License - -Copyright (c) 2019 Martin Chodur - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/MAINTAINERS.md b/MAINTAINERS.md new file mode 100644 index 0000000000000000000000000000000000000000..cd9df82915cb2a1cf95284b5db70cf204f9402cd --- /dev/null +++ b/MAINTAINERS.md @@ -0,0 +1 @@ +* FUSAKLA Martin Chodúr <m.chodur@seznam.cz> diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..9fcb1431fe5d308391237acd80e32d39af739663 --- /dev/null +++ b/Makefile @@ -0,0 +1,23 @@ +# Copyright 2016 The Prometheus Authors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Needs to be defined before including Makefile.common to auto-generate targets +DOCKER_ARCHS ?= amd64 armv7 arm64 + +include Makefile.common + +DOCKER_IMAGE_NAME ?= prometheus-gitlab-notifier + +assets: + @echo ">> writing assets" + @cd $(PREFIX)/asset && GO111MODULE=$(GO111MODULE) $(GO) generate && $(GOFMT) -w assets_vfsdata.go diff --git a/Makefile.common b/Makefile.common new file mode 100644 index 0000000000000000000000000000000000000000..db98993d699ce89ab333bba92d65615073c707b9 --- /dev/null +++ b/Makefile.common @@ -0,0 +1,277 @@ +# Copyright 2018 The Prometheus Authors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +# A common Makefile that includes rules to be reused in different prometheus projects. +# !!! Open PRs only against the prometheus/prometheus/Makefile.common repository! + +# Example usage : +# Create the main Makefile in the root project directory. +# include Makefile.common +# customTarget: +# @echo ">> Running customTarget" +# + +# Ensure GOBIN is not set during build so that promu is installed to the correct path +unexport GOBIN + +GO ?= go +GOFMT ?= $(GO)fmt +FIRST_GOPATH := $(firstword $(subst :, ,$(shell $(GO) env GOPATH))) +GOOPTS ?= +GOHOSTOS ?= $(shell $(GO) env GOHOSTOS) +GOHOSTARCH ?= $(shell $(GO) env GOHOSTARCH) + +GO_VERSION ?= $(shell $(GO) version) +GO_VERSION_NUMBER ?= $(word 3, $(GO_VERSION)) +PRE_GO_111 ?= $(shell echo $(GO_VERSION_NUMBER) | grep -E 'go1\.(10|[0-9])\.') + +GOVENDOR := +GO111MODULE := +ifeq (, $(PRE_GO_111)) + ifneq (,$(wildcard go.mod)) + # Enforce Go modules support just in case the directory is inside GOPATH (and for Travis CI). + GO111MODULE := on + + ifneq (,$(wildcard vendor)) + # Always use the local vendor/ directory to satisfy the dependencies. + GOOPTS := $(GOOPTS) -mod=vendor + endif + endif +else + ifneq (,$(wildcard go.mod)) + ifneq (,$(wildcard vendor)) +$(warning This repository requires Go >= 1.11 because of Go modules) +$(warning Some recipes may not work as expected as the current Go runtime is '$(GO_VERSION_NUMBER)') + endif + else + # This repository isn't using Go modules (yet). + GOVENDOR := $(FIRST_GOPATH)/bin/govendor + endif +endif +PROMU := $(FIRST_GOPATH)/bin/promu +pkgs = ./... + +ifeq (arm, $(GOHOSTARCH)) + GOHOSTARM ?= $(shell GOARM= $(GO) env GOARM) + GO_BUILD_PLATFORM ?= $(GOHOSTOS)-$(GOHOSTARCH)v$(GOHOSTARM) +else + GO_BUILD_PLATFORM ?= $(GOHOSTOS)-$(GOHOSTARCH) +endif + +PROMU_VERSION ?= 0.5.0 +PROMU_URL := https://github.com/prometheus/promu/releases/download/v$(PROMU_VERSION)/promu-$(PROMU_VERSION).$(GO_BUILD_PLATFORM).tar.gz + +GOLANGCI_LINT := +GOLANGCI_LINT_OPTS ?= +GOLANGCI_LINT_VERSION ?= v1.17.1 +# golangci-lint only supports linux, darwin and windows platforms on i386/amd64. +# windows isn't included here because of the path separator being different. +ifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux darwin)) + ifeq ($(GOHOSTARCH),$(filter $(GOHOSTARCH),amd64 i386)) + GOLANGCI_LINT := $(FIRST_GOPATH)/bin/golangci-lint + endif +endif + +PREFIX ?= $(shell pwd) +BIN_DIR ?= $(shell pwd) +DOCKER_IMAGE_TAG ?= $(subst /,-,$(shell git rev-parse --abbrev-ref HEAD)) +DOCKERFILE_PATH ?= ./Dockerfile +DOCKERBUILD_CONTEXT ?= ./ +DOCKER_REPO ?= prom + +DOCKER_ARCHS ?= amd64 + +BUILD_DOCKER_ARCHS = $(addprefix common-docker-,$(DOCKER_ARCHS)) +PUBLISH_DOCKER_ARCHS = $(addprefix common-docker-publish-,$(DOCKER_ARCHS)) +TAG_DOCKER_ARCHS = $(addprefix common-docker-tag-latest-,$(DOCKER_ARCHS)) + +ifeq ($(GOHOSTARCH),amd64) + ifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux freebsd darwin windows)) + # Only supported on amd64 + test-flags := -race + endif +endif + +# This rule is used to forward a target like "build" to "common-build". This +# allows a new "build" target to be defined in a Makefile which includes this +# one and override "common-build" without override warnings. +%: common-% ; + +.PHONY: common-all +common-all: precheck style check_license lint unused build test + +.PHONY: common-style +common-style: + @echo ">> checking code style" + @fmtRes=$$($(GOFMT) -d $$(find . -path ./vendor -prune -o -name '*.go' -print)); \ + if [ -n "$${fmtRes}" ]; then \ + echo "gofmt checking failed!"; echo "$${fmtRes}"; echo; \ + echo "Please ensure you are using $$($(GO) version) for formatting code."; \ + exit 1; \ + fi + +.PHONY: common-check_license +common-check_license: + @echo ">> checking license header" + @licRes=$$(for file in $$(find . -type f -iname '*.go' ! -path './vendor/*') ; do \ + awk 'NR<=3' $$file | grep -Eq "(Copyright|generated|GENERATED)" || echo $$file; \ + done); \ + if [ -n "$${licRes}" ]; then \ + echo "license header checking failed:"; echo "$${licRes}"; \ + exit 1; \ + fi + +.PHONY: common-deps +common-deps: + @echo ">> getting dependencies" +ifdef GO111MODULE + GO111MODULE=$(GO111MODULE) $(GO) mod download +else + $(GO) get $(GOOPTS) -t ./... +endif + +.PHONY: common-test-short +common-test-short: + @echo ">> running short tests" + GO111MODULE=$(GO111MODULE) $(GO) test -short $(GOOPTS) $(pkgs) + +.PHONY: common-test +common-test: + @echo ">> running all tests" + GO111MODULE=$(GO111MODULE) $(GO) test $(test-flags) $(GOOPTS) $(pkgs) + +.PHONY: common-format +common-format: + @echo ">> formatting code" + GO111MODULE=$(GO111MODULE) $(GO) fmt $(pkgs) + +.PHONY: common-vet +common-vet: + @echo ">> vetting code" + GO111MODULE=$(GO111MODULE) $(GO) vet $(GOOPTS) $(pkgs) + +.PHONY: common-lint +common-lint: $(GOLANGCI_LINT) +ifdef GOLANGCI_LINT + @echo ">> running golangci-lint" +ifdef GO111MODULE +# 'go list' needs to be executed before staticcheck to prepopulate the modules cache. +# Otherwise staticcheck might fail randomly for some reason not yet explained. + GO111MODULE=$(GO111MODULE) $(GO) list -e -compiled -test=true -export=false -deps=true -find=false -tags= -- ./... > /dev/null + GO111MODULE=$(GO111MODULE) $(GOLANGCI_LINT) run $(GOLANGCI_LINT_OPTS) $(pkgs) +else + $(GOLANGCI_LINT) run $(pkgs) +endif +endif + +# For backward-compatibility. +.PHONY: common-staticcheck +common-staticcheck: lint + +.PHONY: common-unused +common-unused: $(GOVENDOR) +ifdef GOVENDOR + @echo ">> running check for unused packages" + @$(GOVENDOR) list +unused | grep . && exit 1 || echo 'No unused packages' +else +ifdef GO111MODULE + @echo ">> running check for unused/missing packages in go.mod" + GO111MODULE=$(GO111MODULE) $(GO) mod tidy +ifeq (,$(wildcard vendor)) + @git diff --exit-code -- go.sum go.mod +else + @echo ">> running check for unused packages in vendor/" + GO111MODULE=$(GO111MODULE) $(GO) mod vendor + @git diff --exit-code -- go.sum go.mod vendor/ +endif +endif +endif + +.PHONY: common-build +common-build: promu + @echo ">> building binaries" + GO111MODULE=$(GO111MODULE) $(PROMU) build --prefix $(PREFIX) + +.PHONY: common-tarball +common-tarball: promu + @echo ">> building release tarball" + $(PROMU) tarball --prefix $(PREFIX) $(BIN_DIR) + +.PHONY: common-docker $(BUILD_DOCKER_ARCHS) +common-docker: $(BUILD_DOCKER_ARCHS) +$(BUILD_DOCKER_ARCHS): common-docker-%: + docker build -t "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(DOCKER_IMAGE_TAG)" \ + -f $(DOCKERFILE_PATH) \ + --build-arg ARCH="$*" \ + --build-arg OS="linux" \ + $(DOCKERBUILD_CONTEXT) + +.PHONY: common-docker-publish $(PUBLISH_DOCKER_ARCHS) +common-docker-publish: $(PUBLISH_DOCKER_ARCHS) +$(PUBLISH_DOCKER_ARCHS): common-docker-publish-%: + docker push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(DOCKER_IMAGE_TAG)" + +.PHONY: common-docker-tag-latest $(TAG_DOCKER_ARCHS) +common-docker-tag-latest: $(TAG_DOCKER_ARCHS) +$(TAG_DOCKER_ARCHS): common-docker-tag-latest-%: + docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(DOCKER_IMAGE_TAG)" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:latest" + +.PHONY: common-docker-manifest +common-docker-manifest: + DOCKER_CLI_EXPERIMENTAL=enabled docker manifest create -a "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG)" $(foreach ARCH,$(DOCKER_ARCHS),$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$(ARCH):$(DOCKER_IMAGE_TAG)) + DOCKER_CLI_EXPERIMENTAL=enabled docker manifest push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG)" + +.PHONY: promu +promu: $(PROMU) + +$(PROMU): + $(eval PROMU_TMP := $(shell mktemp -d)) + curl -s -L $(PROMU_URL) | tar -xvzf - -C $(PROMU_TMP) + mkdir -p $(FIRST_GOPATH)/bin + cp $(PROMU_TMP)/promu-$(PROMU_VERSION).$(GO_BUILD_PLATFORM)/promu $(FIRST_GOPATH)/bin/promu + rm -r $(PROMU_TMP) + +.PHONY: proto +proto: + @echo ">> generating code from proto files" + @./scripts/genproto.sh + +ifdef GOLANGCI_LINT +$(GOLANGCI_LINT): + mkdir -p $(FIRST_GOPATH)/bin + curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/$(GOLANGCI_LINT_VERSION)/install.sh \ + | sed -e '/install -d/d' \ + | sh -s -- -b $(FIRST_GOPATH)/bin $(GOLANGCI_LINT_VERSION) +endif + +ifdef GOVENDOR +.PHONY: $(GOVENDOR) +$(GOVENDOR): + GOOS= GOARCH= $(GO) get -u github.com/kardianos/govendor +endif + +.PHONY: precheck +precheck:: + +define PRECHECK_COMMAND_template = +precheck:: $(1)_precheck + +PRECHECK_COMMAND_$(1) ?= $(1) $$(strip $$(PRECHECK_OPTIONS_$(1))) +.PHONY: $(1)_precheck +$(1)_precheck: + @if ! $$(PRECHECK_COMMAND_$(1)) 1>/dev/null 2>&1; then \ + echo "Execution of '$$(PRECHECK_COMMAND_$(1))' command failed. Is $(1) installed?"; \ + exit 1; \ + fi +endef diff --git a/README.md b/README.md index 1143b316a7ab72f270dddeb722e1c9eb8f3a559a..f0c0e17674119ccdfa10d980bbae09d6e27ad7e0 100644 --- a/README.md +++ b/README.md @@ -1 +1,92 @@ -# prometheus-gitlab-notifier \ No newline at end of file +# Prometheus Gitlab Notifier + +Tool which implements [Alertmanager](https://github.com/prometheus/alertmanager) webhook notifier +and creates Gitlab issue with the alert metadata. + +For new features or changes check out the [CHANGELOG.md](./CHANGELOG.md) + +### How to run it +``` +$ ./prometheus-gitlab-notifier --help +usage: prometheus-gitlab-notifier --gitlab.url=GITLAB.URL --gitlab.token.file=GITLAB.TOKEN.FILE --project.id=PROJECT.ID [<flags>] + +Web server listening for webhooks of alertmanager and creating an issue in Gitlab based on it. + +Flags: + --help Show context-sensitive help (also try --help-long and --help-man). + --debug Enables debug logging. + --server.addr="0.0.0.0:9288" Allows to change the address and port at which the server will listen for incoming connections. + --gitlab.url=GITLAB.URL URL of the Gitlab API. + --gitlab.token.file=GITLAB.TOKEN.FILE + Path to file containing gitlab token. + --project.id=PROJECT.ID Id of project where to create the issues. + --group.interval=1h Duration how long back to check for opened issues with the same group labels to append the new alerts to (go duration syntax allowing 'ns', 'us' , 'ms', 's', 'm', 'h'). + --issue.label=ISSUE.LABEL ... Labels to add to the created issue. (Can be passed multiple times) + --dynamic.issue.label.name=DYNAMIC.ISSUE.LABEL.NAME ... + Alert label, which is to be propagated to the resulting Gitlab issue as scoped label if present in the received alert. (Can be passed multiple times) + --issue.template=conf/default_issue.tmpl + Path to the issue golang template file. + --queue.size.limit=100 Limit of the alert queue size. + --retry.backoff=5m Duration how long to wait till next retry (go duration syntax allowing 'ns', 'us' , 'ms', 's', 'm', 'h'). + --retry.limit=5 Maximum number of retries for single alert. If exceeded it's thrown away. + --graceful.shutdown.wait.duration=30s + Duration how long to wait on graceful shutdown marked as not ready (go duration syntax allowing 'ns', 'us' , 'ms', 's', 'm', 'h'). +``` + +To test it is running check logs or http://0.0.0.0:9288/readiness + +### Issue template +Look of the resulting issue in Gitlab can be customized using [Go template]{https://golang.org/pkg/text/template/}. +Default template can be found in [conf/default_issue.tmpl](conf/default_issue.tmpl). +The available data during templating is the Alertmanager webhook message struct itself. +Example can be found in [conf/alert.json](conf/alert.json). +To use own template override the default one with the `--issue.template` flag. +> The template is validated on startup but if even after validation the templating +fails in the runtime, raw JSON of the alert will be pasted to the text of the issue as a fallback. + +Example of the default template: + + + +### Configure Alertmanager +You just need to add the [`<webhook_config>`](https://prometheus.io/docs/alerting/configuration/#webhook_config) +receiver to your Alertmanager configuration and disable sending resolved notifications with `send_resolved: false`. +Also better to set the `repeat_interval` to higher value since every retry will create new issue. + +See the minimal example in the [conf/alertmanager_conf.yaml](conf/alertmanager_conf.yaml). + + +### Issue labeling scheme +The Gitlab notifier allows to label the resulting issue based on the alert labels. +It uses mostly Gitlab scoped labels in format `label::value`. +The grouping labels of the alert are added to the issue automatically to allow identifying same +alerts (more on that in [Grouping](#Grouping) section). +Additionally you can specify names of labels to be also added to the issue using flag `--dynamic.issue.label.name`. +Last thing you can add are static labels which will be added to every issue using flag `--issue.label`, + + +### Grouping +To avoid flooding gitlab with identical alerts if they happen to fire and resolve again and again, +Gitlab notifier checks for issues witch the same grouping labels as the new incoming alert. +If if finds any still open issue younger than `1h` by default (can be controlled by flag `--group.interval`), +it only appends the rendered template to the end of the issue description +and adds to the issue label `appended-alerts::<number>` witch count of how many times it was updated. + + +### Deployment +Example kubernetes manifests can be found at [kubernetes/](./kubernetes) + + +### Instrumentation + +- `/liveness`: liveness endpoint returns always 200 +- `/readiness`: tries HEAD request to the configured Gitlab URL and fails if it does not succeeds. +- `/metrics`: metrics endpoint returning app runtime metrics in Prometheus format + +### How to contribute and release + +**Contributing:** +1. Implement your changes and test them on your own testing repository. +1. Add note about changes made to the [CHANGELOG.md](CHANGELOG.md) `[Unreleased]` section. +1. Create PR and apply for CR from maintainers. +1. Get it merged. diff --git a/VERSION b/VERSION new file mode 100644 index 0000000000000000000000000000000000000000..a918a2aa18d5bec6a8bb93891a7a63c243111796 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.6.0 diff --git a/cmd/prometheus-gitlab-notifier/prometheus-gitlab-notifier.go b/cmd/prometheus-gitlab-notifier/prometheus-gitlab-notifier.go new file mode 100644 index 0000000000000000000000000000000000000000..7741b76ee81959ee76c00d0beaa3e2b6c4834c4a --- /dev/null +++ b/cmd/prometheus-gitlab-notifier/prometheus-gitlab-notifier.go @@ -0,0 +1,189 @@ +// Copyright 2019 FUSAKLA Martin Chodúr +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "io/ioutil" + "net/http" + "os" + "os/signal" + "strings" + "syscall" + "text/template" + "time" + + "github.com/alecthomas/kingpin" + "github.com/fusakla/prometheus-gitlab-notifier/pkg/alertmanager" + "github.com/fusakla/prometheus-gitlab-notifier/pkg/api" + "github.com/fusakla/prometheus-gitlab-notifier/pkg/gitlab" + "github.com/fusakla/prometheus-gitlab-notifier/pkg/handler" + "github.com/fusakla/prometheus-gitlab-notifier/pkg/metrics" + "github.com/fusakla/prometheus-gitlab-notifier/pkg/prober" + "github.com/fusakla/prometheus-gitlab-notifier/pkg/processor" + "github.com/go-kit/kit/log" + "github.com/go-kit/kit/log/level" + "github.com/gorilla/mux" + "github.com/pkg/errors" +) + +func setupLogger(debug bool) log.Logger { + l := log.NewLogfmtLogger(log.NewSyncWriter(os.Stdout)) + l = log.With(l, "ts", log.DefaultTimestamp, "caller", log.DefaultCaller) + if debug { + l = level.NewFilter(l, level.AllowDebug()) + } else { + l = level.NewFilter(l, level.AllowInfo()) + } + return l +} + +func waitForEmptyChannel(logger log.Logger, ch <-chan *alertmanager.Webhook) { + level.Info(logger).Log("msg", "waiting for all the alerts to be processed") + for { + if len(ch) > 0 { + level.Info(logger).Log("msg", "there are still alerts in the queue, waiting forthem to be processed", "queue_size", len(ch)) + time.Sleep(10 * time.Millisecond) + continue + } + break + } + level.Info(logger).Log("msg", "processing of the rest of alerts is done") +} + +func startServer(logger log.Logger, r http.Handler) (*http.Server, <-chan error) { + errCh := make(chan error, 1) + srv := &http.Server{ + Handler: handler.Instrumented(logger, r), + Addr: *serverAddr, + WriteTimeout: 5 * time.Second, + ReadTimeout: 5 * time.Second, + } + go func() { + defer close(errCh) + level.Info(logger).Log("msg", "Starting prometheus-gitlab-notifier", "addr", "0.0.0.0:9288") + if err := srv.ListenAndServe(); err != nil { + if err != http.ErrServerClosed { + level.Error(logger).Log("msg", "server failed", "error", err) + errCh <- err + } + } + }() + return srv, errCh +} + +var ( + app = kingpin.New("prometheus-gitlab-notifier", "Web server listening for webhooks of alertmanager and creating an issue in Gitlab based on it.") + debug = app.Flag("debug", "Enables debug logging.").Bool() + serverAddr = app.Flag("server.addr", "Allows to change the address and port at which the server will listen for incoming connections.").Default("0.0.0.0:9288").String() + gitlabURL = app.Flag("gitlab.url", "URL of the Gitlab API.").Required().String() + gitlabTokenFile = app.Flag("gitlab.token.file", "Path to file containing gitlab token.").Required().ExistingFile() + projectId = app.Flag("project.id", "Id of project where to create the issues.").Required().Int() + groupInterval = app.Flag("group.interval", "Duration how long back to check for opened issues with the same group labels to append the new alerts to (go duration syntax allowing 'ns', 'us' , 'ms', 's', 'm', 'h').").Default("1h").Duration() + issueLabels = app.Flag("issue.label", "Labels to add to the created issue. (Can be passed multiple times)").Strings() + dynamicIssueLabels = app.Flag("dynamic.issue.label.name", "Alert label, which is to be propagated to the resulting Gitlab issue as scoped label if present in the received alert. (Can be passed multiple times)").Strings() + issueTemplatePath = app.Flag("issue.template", "Path to the issue golang template file.").Default("conf/default_issue.tmpl").ExistingFile() + queueSizeLimit = app.Flag("queue.size.limit", "Limit of the alert queue size.").Default("100").Int() + retryBackoff = app.Flag("retry.backoff", "Duration how long to wait till next retry (go duration syntax allowing 'ns', 'us' , 'ms', 's', 'm', 'h').").Default("5m").Duration() + retryLimit = app.Flag("retry.limit", "Maximum number of retries for single alert. If exceeded it's thrown away.").Default("5").Int() + gracefulShutdownWait = app.Flag("graceful.shutdown.wait.duration", "Duration how long to wait on graceful shutdown marked as not ready (go duration syntax allowing 'ns', 'us' , 'ms', 's', 'm', 'h').").Default("30s").Duration() +) + +func main() { + + kingpin.MustParse(app.Parse(os.Args[1:])) + + // Initiate logging. + logger := setupLogger(*debug) + + // Initiate Gitlab client. + gitlabIssueTextTemplate, err := template.ParseFiles(*issueTemplatePath) + if err != nil { + level.Error(logger).Log("msg", "invalid gitlab issue template", "file", *issueTemplatePath, "err", err) + os.Exit(1) + } + token, err := ioutil.ReadFile(*gitlabTokenFile) + if err != nil { + level.Error(logger).Log("msg", "failed to read token file", "file", gitlabTokenFile, "err", err) + os.Exit(1) + } + g, err := gitlab.New( + log.With(logger, "component", "gitlab"), + *gitlabURL, + strings.TrimSpace(string(token)), + *projectId, + gitlabIssueTextTemplate, + issueLabels, + dynamicIssueLabels, + groupInterval, + ) + if err != nil { + level.Error(logger).Log("msg", "invalid gitlab configuration") + os.Exit(1) + } + + // Start processing all incoming alerts. + alertChan := make(chan *alertmanager.Webhook, *queueSizeLimit) + proc := processor.New(log.With(logger, "component", "processor")) + processCtx, processCancelFunc := context.WithCancel(context.Background()) + defer processCancelFunc() + proc.Process(processCtx, g, alertChan, *retryLimit, *retryBackoff) + + // Setup routing for HTTP server. + r := mux.NewRouter() + // Initialize the main API. + webhookApi := api.NewInRouter( + log.With(logger, "component", "api"), + r.PathPrefix("/api").Subrouter(), + alertChan, + ) + // Initialize prober providing readiness and liveness checks. + readinessProber := prober.NewInRouter( + log.With(logger, "component", "prober"), + r.PathPrefix("/").Subrouter(), + ) + // Initialize metrics handler to serve Prometheus metrics. + metrics.HandleInRouter(r) + + // Start HTTP server + _, serverErrorChan := startServer(log.With(logger, "component", "server"), r) + + // Subscribe to system signals so we can react on them with graceful termination. + gracefulStop := make(chan os.Signal, 2) + signal.Notify(gracefulStop, syscall.SIGTERM) + signal.Notify(gracefulStop, syscall.SIGINT) + + // It the server fails or we receive signal to gracefully shut down we wait till the alert queue is processed(empty). + for { + select { + case <-serverErrorChan: + // If server failed just wait for all the alerts to be processed. + waitForEmptyChannel(logger, alertChan) + os.Exit(1) + case sig := <-gracefulStop: + level.Info(logger).Log("msg", "received system signal for graceful shutdown", "signal", sig) + // Mark server as not ready so no new connections will come. + readinessProber.SetServerNotReady(errors.New("server is shutting down")) + // Wait for specified time after marking server not ready so the environment can react on it. + level.Info(logger).Log("msg", "waiting for graceful shutdown", "duration", gracefulShutdownWait) + time.Sleep(*gracefulShutdownWait) + // Stop receiving new alerts. + webhookApi.Close() + // Wait for all enqueued alerts to be processed. + waitForEmptyChannel(logger, alertChan) + os.Exit(0) + } + } + +} diff --git a/conf/alert.json b/conf/alert.json new file mode 100644 index 0000000000000000000000000000000000000000..81a1e80ec92371b32bc738f24bde5edc9b1ca98f --- /dev/null +++ b/conf/alert.json @@ -0,0 +1,32 @@ +{ + "version": "1", + "groupKey": "meh", + "receiver": "test-receiver", + "status": "firing", + "alerts": [ + { + "status": "test", + "labels": { + "app": "test" + }, + "annotations": { + "description": "this is a testing alert" + }, + "startsAt": "2018-09-22T12:42:31Z", + "endsAt": "2018-09-22T12:42:31Z", + "generatorURL": "http://test.com" + } + ], + "groupLabels": { + "label": "value" + }, + "commonLabels": { + "locality": "nagano", + "severity": "warning", + "alertname": "ThisIsATestingAlert" + }, + "commonAnnotations": { + "title": "Something is wrong!!!" + }, + "externalURL": "http://test.com" +} diff --git a/conf/alertmanager_conf.yaml b/conf/alertmanager_conf.yaml new file mode 100644 index 0000000000000000000000000000000000000000..1e3044d65d718aca4999876e231c6595f16e0f31 --- /dev/null +++ b/conf/alertmanager_conf.yaml @@ -0,0 +1,9 @@ +route: + repeat_interval: 10000h + receiver: PrometheusGitlabNotifier + + receivers: + - name: PrometheusGitlabNotifier + webhook_configs: + - send_resolved: false + url: http://0.0.0.0:9288/api/alertmanager diff --git a/conf/default_issue.tmpl b/conf/default_issue.tmpl new file mode 100644 index 0000000000000000000000000000000000000000..d05b890fbb085838ee609407b4603664fddcda3e --- /dev/null +++ b/conf/default_issue.tmpl @@ -0,0 +1,31 @@ +{{define "alert"}} + - **`{{ index .Annotations "description" }}`** + - **Starts at**: {{ .StartsAt }} + - **Ends at**: {{ .EndsAt }} + - **Generator URL**: [{{ .GeneratorURL }}]({{ .GeneratorURL }}) + - **Labels**: `{{`{`}}{{ range $k,$v := .Labels }}{{$k}}="{{$v}}", {{end}}{{`}`}}` +{{end}} + + +# `{{ index .CommonLabels "severity" }}` alert `{{ index .CommonLabels "alertname" }}` occurred +**Title:** {{ index .CommonAnnotations "title" }} +**Alertmanager link:** [{{ .ExternalURL }}]({{ .ExternalURL }}) + +### Common labels: +{{- range $k,$v := .CommonLabels }} + - **`{{ $k }}`**: `{{ $v }}` +{{- end }} + +### Common annotations: +{{- range $k,$v := .CommonAnnotations }} + {{- if and (not (eq $k "title")) (not (eq $k "description")) }} + - **`{{ $k }}`**: `{{ $v }}` + {{- end }} +{{- end }} + +--- + +## Alerts +{{- range .Alerts }} + {{ template "alert" . }} +{{- end }} diff --git a/conf/issue_example.png b/conf/issue_example.png new file mode 100644 index 0000000000000000000000000000000000000000..1fc2427e13ff018d242713f6e8b42597a49344f9 Binary files /dev/null and b/conf/issue_example.png differ diff --git a/errcheck_excludes.txt b/errcheck_excludes.txt new file mode 100644 index 0000000000000000000000000000000000000000..97b977141bb32b71933ad87d7c985bedf10728d7 --- /dev/null +++ b/errcheck_excludes.txt @@ -0,0 +1 @@ +(github.com/go-kit/kit/log.Logger).Log diff --git a/go.mod b/go.mod new file mode 100644 index 0000000000000000000000000000000000000000..f870b07e4ead883facf5623cb86900d9ff08b981 --- /dev/null +++ b/go.mod @@ -0,0 +1,13 @@ +module github.com/fusakla/prometheus-gitlab-notifier + +go 1.12 + +require ( + github.com/alecthomas/kingpin v2.2.6+incompatible + github.com/go-kit/kit v0.8.0 + github.com/gorilla/mux v1.7.2 + github.com/pkg/errors v0.8.0 + github.com/prometheus/alertmanager v0.17.1-0.20190619131440-bef850ac905c + github.com/prometheus/client_golang v1.0.0 + github.com/xanzy/go-gitlab v0.18.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000000000000000000000000000000000000..f4a26cf92edef7703bdcbb9fc0e260d719ad85b6 --- /dev/null +++ b/go.sum @@ -0,0 +1,195 @@ +github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/alecthomas/kingpin v2.2.6+incompatible h1:5svnBTFgJjZvGKyYBtMB0+m5wvrbUHiqye8wRJMlnYI= +github.com/alecthomas/kingpin v2.2.6+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da h1:8GUt8eRujhVEGZFFEjBj46YV4rDjvGrNxb0KMWYkL2I= +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/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/cenkalti/backoff v0.0.0-20181003080854-62661b46c409 h1:Da6uN+CAo1Wf09Rz1U4i9QN8f0REjyNJ73BEwAq/paU= +github.com/cenkalti/backoff v0.0.0-20181003080854-62661b46c409/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/cespare/xxhash v0.0.0-20181017004759-096ff4a8a059 h1:o4GWHLIzU2GCL0R5PZVFpVdPCGmzBH0tXXZlZ78QddA= +github.com/cespare/xxhash v0.0.0-20181017004759-096ff4a8a059/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +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/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/go-kit/kit v0.8.0 h1:Wz+5lgoB0kkuqLEc6NVmwRknTKP6dTGbSqvhZtBI/j0= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0 h1:8HUsc87TaSWLKwrnumgC8/YconD2fJQsRJAsWaPg2ic= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI= +github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= +github.com/go-openapi/analysis v0.17.2/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= +github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= +github.com/go-openapi/errors v0.17.2/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= +github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= +github.com/go-openapi/jsonpointer v0.17.2/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= +github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/jsonreference v0.17.2/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.17.2/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA= +github.com/go-openapi/runtime v0.18.0/go.mod h1:uI6pHuxWYTy94zZxgcwJkUWa9wbIlhteGfloI10GD4U= +github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/spec v0.17.2/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= +github.com/go-openapi/strfmt v0.17.2/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= +github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= +github.com/go-openapi/swag v0.17.2/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= +github.com/go-openapi/validate v0.17.2/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4= +github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= +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 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c h1:964Od4U6p2jUkFxvCydnIczKteheJEzHRToSGK3Bnlw= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.7.2 h1:zoNxOV7WjqXptQOVngLmcSQgXmgk4NMz1HibBchjl/I= +github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3 h1:zKjpN5BK/P5lMYrLmBHdBULWbJ0XpYR+7NGzqkZzoD4= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= +github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= +github.com/hashicorp/go-uuid v1.0.0 h1:RS8zrF7PhGwyNPOtxSClXXj9HA8feRnJzgnI1RJCSnM= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/memberlist v0.1.3 h1:EmmoJme1matNzb+hMpDuR/0sbJSUisxyqBGG676r31M= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/jessevdk/go-flags v0.0.0-20180331124232-1c38ed7ad0cc/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +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/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY= +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/kylelemons/godebug v0.0.0-20160406211939-eadb3ce320cb/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= +github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +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/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 h1:9jZdLNd/P4+SfEJ0TNyxYpsK8N4GtfylBLqtbYN1sbA= +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-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223 h1:F9x/1yl3T2AeKLr2AMdilSD8+f9bvMnNN8VS5iDtovc= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +github.com/oklog/ulid v0.0.0-20170117200651-66bb6560562f h1:UpfE/Q64+1idrbE+phdstApLr3SJBSjkxg8AvRx1mSk= +github.com/oklog/ulid v0.0.0-20170117200651-66bb6560562f/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c h1:Lgl0gzECD8GnQ5QCWA8o6BtfL6mDH5rQgM4/fX3avOs= +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/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= +github.com/pkg/errors v0.8.0/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/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/prometheus/alertmanager v0.17.1-0.20190619131440-bef850ac905c h1:NT8YaSNAgl5RYWyL/i05KO+R8yCq2mn1NLhUPFdnaoU= +github.com/prometheus/alertmanager v0.17.1-0.20190619131440-bef850ac905c/go.mod h1:WcxHBl40VSPuOaqWae6l6HpnEOVRIycEJ7i9iYkadEE= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0 h1:vrDKnkGzuGvhNAL56c7DBz29ZL+KxnoR0x7enabFceM= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.4.1 h1:K0MGApIoQvMw27RTdJkPbr3JZ7DNbtxQNyi5STVM6Kw= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2 h1:6LJUbpNm42llc4HRCuvApCSWB/WfhuNo9K98Q9sNGfs= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/prometheus v0.0.0-20180315085919-58e2a31db8de/go.mod h1:oAIUtOny2rjMX0OWN5vPR5/q/twIROJvdqnQKDdil/s= +github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/satori/go.uuid v0.0.0-20160603004225-b111a074d5ef h1:RoeI7K0oZIcUirMHsFpQjTVDrl1ouNh8T7v3eNsUxL0= +github.com/satori/go.uuid v0.0.0-20160603004225-b111a074d5ef/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371 h1:SWV2fHctRpRrp49VXJ6UZja7gU9QLHwRpIPBN89SKEo= +github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= +github.com/shurcooL/vfsgen v0.0.0-20180825020608-02ddb050ef6b h1:rKVW5h3pEu8gGxD+ZlOmBvFYAxXLCYeQv/eg+t6QvLQ= +github.com/shurcooL/vfsgen v0.0.0-20180825020608-02ddb050ef6b/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +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 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/xanzy/go-gitlab v0.18.0 h1:LybNSWSIw8BK+GnxuETAhUXEzzh5rHsHjopqVkGJXRE= +github.com/xanzy/go-gitlab v0.18.0/go.mod h1:LSfUQ9OPDnwRqulJk2HcWaAiFfCzaknyeGvjQI67MbE= +github.com/xlab/treeprint v0.0.0-20180616005107-d6fb6747feb6/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3 h1:KYQXGkl6vs02hK7pK4eIbw0NpNPedieTSTEiJ//bwGs= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181005035420-146acd28ed58/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-20181108082009-03003ca0c849/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 h1:a3CU5tJYVj92DY2LaA1kUkrsqD5/3mLDhx2NcNqyW+0= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288 h1:JIqe8uIcRBHXDQVvZtHwp80ai3Lw3IJAeJEs55Dc1W0= +golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +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/sys v0.0.0-20180823144017-11551d06cbcc/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-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5 h1:mzjBh+S5frKOsOBobWIMAbXavqjmgO17k/2puhcFR94= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180805044716-cb6730876b98/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190118193359-16909d206f00 h1:6OmoTtlNJlHuWNIjTEyUtMBHrryp8NRuf/XtnC7MmXM= +golang.org/x/tools v0.0.0-20190118193359-16909d206f00/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +google.golang.org/appengine v1.3.0 h1:FBSsiFRMz3LBeXIomRnVzrQwSDj4ibvcRexLG0LZGQk= +google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +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 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/kubernetes/gitlab-token-secret.yaml b/kubernetes/gitlab-token-secret.yaml new file mode 100644 index 0000000000000000000000000000000000000000..a75ca85d2032e89e7b3dda77a02e2985ffcfe225 --- /dev/null +++ b/kubernetes/gitlab-token-secret.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Secret +metadata: + name: prometheus-gitlab-notifier-gitlab-token +data: + gitlab_token: "Y2hhY2hh" diff --git a/kubernetes/issue-template-configmap.yaml b/kubernetes/issue-template-configmap.yaml new file mode 100644 index 0000000000000000000000000000000000000000..1fa8b02c5a714ce29d0588346b86eba0c040fd08 --- /dev/null +++ b/kubernetes/issue-template-configmap.yaml @@ -0,0 +1,37 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: prometheus-gitlab-notifier-issue-template +data: + issue.tmpl: | + {{define "alert"}} + - **`{{ index .Annotations "description" }}`** + - **Starts at**: {{ .StartsAt }} + - **Ends at**: {{ .EndsAt }} + - **Generator URL**: [{{ .GeneratorURL }}]({{ .GeneratorURL }}) + - **Labels**: `{{`{`}}{{ range $k,$v := .Labels }}{{$k}}="{{$v}}", {{end}}{{`}`}}` + {{end}} + + + # `{{ index .CommonLabels "severity" }}` alert `{{ index .CommonLabels "alertname" }}` occurred + **Title:** {{ index .CommonAnnotations "title" }} + **Alertmanager link:** [{{ .ExternalURL }}]({{ .ExternalURL }}) + + ### Common labels: + {{- range $k,$v := .CommonLabels }} + - **`{{ $k }}`**: `{{ $v }}` + {{- end }} + + ### Common annotations: + {{- range $k,$v := .CommonAnnotations }} + {{- if and (not (eq $k "title")) (not (eq $k "description")) }} + - **`{{ $k }}`**: `{{ $v }}` + {{- end }} + {{- end }} + + --- + + ## Alerts + {{- range .Alerts }} + {{ template "alert" . }} + {{- end }} diff --git a/kubernetes/prometheus-gitlab-notifier-deployment.yaml b/kubernetes/prometheus-gitlab-notifier-deployment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..25f44447089b4a7c228e89596765bb1cbe73d0d9 --- /dev/null +++ b/kubernetes/prometheus-gitlab-notifier-deployment.yaml @@ -0,0 +1,55 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: prometheus-gitlab-notifier +spec: + selector: + matchLabels: + app: prometheus-gitlab-notifier + replicas: 2 + template: + metadata: + labels: + app: prometheus-gitlab-notifier + spec: + containers: + - name: prometheus-gitlab-notifier + image: fusakla/prometheus-gitlab-notifier:0.6.0 + args: + - "--gitlab.url=https://gitlab.com/api/v4" + - "--project.id=13766104" + - "--issue.label=automated-alert-issue" + - "--group.interval=168h" # 7d + - "--issue.template=/prometheus-gitlab-notifier/issue-templates/issue.tmpl" + - "--gitlab.token.file=/prometheus-gitlab-notifier/secrets/gitlab_token" + readinessProbe: + httpGet: + port: 9288 + path: /readiness + livenessProbe: + httpGet: + port: 9288 + path: /liveness + ports: + - containerPort: 9288 + resources: + requests: + cpu: "50m" + memory: "50Mi" + limits: + cpu: "500m" + memory: "512Mi" + volumeMounts: + - name: issue-template + readOnly: true + mountPath: "/prometheus-gitlab-notifier/issue-templates/" + - name: gitlab-token + readOnly: true + mountPath: "/prometheus-gitlab-notifier/secrets" + volumes: + - name: issue-template + configMap: + name: prometheus-gitlab-notifier-issue-template + - name: gitlab-token + secret: + secretName: prometheus-gitlab-notifier-gitlab-token diff --git a/kubernetes/prometheus-gitlab-notifier-service.yaml b/kubernetes/prometheus-gitlab-notifier-service.yaml new file mode 100644 index 0000000000000000000000000000000000000000..18ff7f95becdc384ca7daad7dc1a98d5cb3de203 --- /dev/null +++ b/kubernetes/prometheus-gitlab-notifier-service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: prometheus-gitlab-notifier + labels: + app: prometheus-gitlab-notifier +spec: + ports: + - port: 9288 + protocol: TCP + selector: + app: prometheus-gitlab-notifier diff --git a/pkg/alertmanager/webhook.go b/pkg/alertmanager/webhook.go new file mode 100644 index 0000000000000000000000000000000000000000..7093d5ecd26cb2d71b501f50c5b779e690194d3d --- /dev/null +++ b/pkg/alertmanager/webhook.go @@ -0,0 +1,49 @@ +// Copyright 2019 FUSAKLA Martin Chodúr +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package alertmanager + +import ( + "sync" + + "github.com/prometheus/alertmanager/notify/webhook" +) + +// NewWebhookFromAlertmanagerMessage returns new Webhook wrapping the original Alertmanager webhook.message. +func NewWebhookFromAlertmanagerMessage(message webhook.Message) *Webhook { + return &Webhook{ + Message: message, + retryCount: 0, + } +} + +// Webhook is wrapper for the Alertmanager webhook.message adding retry counter. +type Webhook struct { + webhook.Message + retryCount int + retryMtx sync.RWMutex +} + +// Retry increments number of retries for the Webhook. +func (w *Webhook) Retry() { + w.retryMtx.Lock() + defer w.retryMtx.Unlock() + w.retryCount++ +} + +// RetryCount returns number of retries for the given alertmanager message. +func (w *Webhook) RetryCount() int { + w.retryMtx.RLock() + defer w.retryMtx.RUnlock() + return w.retryCount +} diff --git a/pkg/api/api.go b/pkg/api/api.go new file mode 100644 index 0000000000000000000000000000000000000000..5acd4b8a7153396b220c03d52b52d1c6e0180ce6 --- /dev/null +++ b/pkg/api/api.go @@ -0,0 +1,86 @@ +// Copyright 2019 FUSAKLA Martin Chodúr +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "sync" + + "github.com/fusakla/prometheus-gitlab-notifier/pkg/alertmanager" + + "github.com/go-kit/kit/log" + "github.com/go-kit/kit/log/level" + "github.com/gorilla/mux" + "github.com/prometheus/alertmanager/notify/webhook" +) + +// NewInRouter creates new Api instance which will register it's handlers in the given router. +func NewInRouter(logger log.Logger, r *mux.Router, ch chan<- *alertmanager.Webhook) *Api { + api := &Api{ + logger: logger, + alertChan: ch, + receiveAlerts: true, + } + api.registerHandlers(r) + return api +} + +// Api defines handler functions for receiving Alertmanager endpoints. +type Api struct { + logger log.Logger + alertChan chan<- *alertmanager.Webhook + receiveAlerts bool + receiveAlertsMtx sync.RWMutex +} + +func (a *Api) registerHandlers(router *mux.Router) { + router.HandleFunc("/alertmanager", a.webhookHandler) +} + +func (a *Api) webhookHandler(w http.ResponseWriter, r *http.Request) { + if !a.canReceiveAlerts() { + http.Error(w, "Server is not receiving new alerts.", http.StatusServiceUnavailable) + return + } + var message webhook.Message + err := json.NewDecoder(r.Body).Decode(&message) + if err != nil { + http.Error(w, fmt.Sprintf("Invalid incomming webhook format. Failed with error: %s", err), http.StatusBadRequest) + return + } + + // Push the message to channel + a.alertChan <- alertmanager.NewWebhookFromAlertmanagerMessage(message) + level.Debug(a.logger).Log("msg", "enqueued alert for processing", "group_key", message.GroupKey) + + w.WriteHeader(http.StatusOK) + _, _ = io.WriteString(w, `Ok, Alert enqueued.`) +} + +// Close disabled receiving of new alerts in the API used mainly for graceful shutdown. +func (a *Api) Close() { + a.receiveAlertsMtx.Lock() + defer a.receiveAlertsMtx.Unlock() + a.receiveAlerts = false + close(a.alertChan) +} + +func (a *Api) canReceiveAlerts() bool { + a.receiveAlertsMtx.RLock() + defer a.receiveAlertsMtx.RUnlock() + return a.receiveAlerts +} diff --git a/pkg/gitlab/gitlab.go b/pkg/gitlab/gitlab.go new file mode 100644 index 0000000000000000000000000000000000000000..69942c7de5c6635bfe3e6c93b71fccd239336e74 --- /dev/null +++ b/pkg/gitlab/gitlab.go @@ -0,0 +1,249 @@ +// Copyright 2019 FUSAKLA Martin Chodúr +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gitlab + +import ( + "bufio" + "bytes" + "encoding/json" + "fmt" + "net/http" + "regexp" + "strconv" + "text/template" + "time" + + "github.com/fusakla/prometheus-gitlab-notifier/pkg/alertmanager" + "github.com/fusakla/prometheus-gitlab-notifier/pkg/metrics" + "github.com/go-kit/kit/log" + "github.com/go-kit/kit/log/level" + "github.com/xanzy/go-gitlab" +) + +// New creates new Gitlab instance configured to work with specified gitlab instance, project and with given authentication. +func New(logger log.Logger, url string, token string, projectId int, issueTemplate *template.Template, issueLabels *[]string, dynamicIssueLabels *[]string, groupInterval *time.Duration) (*Gitlab, error) { + cli := gitlab.NewClient(nil, token) + if err := cli.SetBaseURL(url); err != nil { + level.Error(logger).Log("msg", "invalid Gitlab URL", "url", url, "err", "err") + return nil, err + } + g := &Gitlab{ + client: cli, + projectId: projectId, + issueTemplate: issueTemplate, + issueLabels: issueLabels, + dynamicIssueLabels: dynamicIssueLabels, + groupInterval: groupInterval, + logger: logger, + } + if err := g.ping(); err != nil { + level.Error(logger).Log("msg", "cannot reach the Gitlab", "url", url, "err", "err") + return nil, err + } + return g, nil +} + +// Gitlab holds configured Gitlab client and provides API for creating templated issue from the Webhook. +type Gitlab struct { + client *gitlab.Client + projectId int + issueTemplate *template.Template + issueLabels *[]string + dynamicIssueLabels *[]string + groupInterval *time.Duration + logger log.Logger +} + +func (g *Gitlab) formatGitlabScopedLabel(key string, value string) string { + return fmt.Sprintf("%s::%s", key, value) +} + +func (g *Gitlab) extractDynamicLabels(msg *alertmanager.Webhook) []string { + var labelsMap = map[string]string{} + for _, a := range msg.Alerts { + for k, v := range a.Labels { + for _, l := range *g.dynamicIssueLabels { + if k == l { + labelsMap[k] = v + } + } + } + } + var resLabels []string + for k, v := range labelsMap { + resLabels = append(resLabels, g.formatGitlabScopedLabel(k, v)) + } + return resLabels +} + +func (g *Gitlab) extractGroupingLabels(msg *alertmanager.Webhook) []string { + var resLabels []string + for k, v := range msg.GroupLabels { + resLabels = append(resLabels, g.formatGitlabScopedLabel(k, v)) + } + return resLabels +} + +func (g *Gitlab) renderIssueTemplate(msg *alertmanager.Webhook) (*bytes.Buffer, error) { + var issueText bytes.Buffer + // Try to template the issue text template with the alert data. + if err := g.issueTemplate.Execute(&issueText, msg.Data); err != nil { + // As a fallback we try to add raw JSON of the alert to the issue text so we don't miss an alert just because of template error. + metrics.ReportError("IssueTemplateError", "") + level.Error(g.logger).Log("msg", "failed to template issue text, using pure JSON instead", "err", err) + w := bufio.NewWriter(&issueText) + if err := json.NewEncoder(w).Encode(msg); err != nil { + // If even JSON marshalling fails we return error + metrics.ReportError("JSONMarshalError", "") + level.Error(g.logger).Log("msg", "failed to marshall alert to JSON", "err", err) + return nil, err + } + } + return &issueText, nil +} + +func (g *Gitlab) getOpenIssuesSince(groupingLabels []string, sinceTime time.Time) ([]*gitlab.Issue, error) { + openState := "opened" + scope := "created_by_me" + orderBy := "created_at" + listOpts := gitlab.ListIssuesOptions{ + Labels: groupingLabels, + CreatedAfter: &sinceTime, + State: &openState, + Scope: &scope, + OrderBy: &orderBy, + } + issues, response, err := g.client.Issues.ListIssues(&listOpts) + if err != nil { + metrics.ReportError("ListGitlabIssuesError", "gitlab") + level.Error(g.logger).Log("msg", "failed to list gitlab issues with", "opts", listOpts, "response", response, "err", err) + return []*gitlab.Issue{}, err + } + return issues, nil +} + +func (g *Gitlab) getTimeBefore(before *time.Duration) time.Time { + return time.Now().Local().Add(-*before) +} + +func (g *Gitlab) createGitlabIssue(msg *alertmanager.Webhook, groupingLabels []string, issueText *bytes.Buffer) error { + // Collect all new issue labels + labels := *g.issueLabels + labels = append(labels, groupingLabels...) + labels = append(labels, g.extractDynamicLabels(msg)...) + options := &gitlab.CreateIssueOptions{ + Title: gitlab.String(fmt.Sprintf("Firing alert `%s`", msg.CommonLabels["alertname"])), + Description: gitlab.String(issueText.String()), + Labels: labels, + } + + createdIssue, response, err := g.client.Issues.CreateIssue(g.projectId, options) + if err != nil { + metrics.ReportError("FailedToCreateGitlabIssue", "gitlab") + level.Error(g.logger).Log("msg", "failed to create gitlab issue", "err", err, "response", response) + return err + } + level.Info(g.logger).Log("msg", "created issue in gitlab", "gitlab_issue_id", createdIssue.IID, "alert_grouping_key", msg.GroupKey) + return nil +} + +func (g *Gitlab) increaseAppendLabel(labels []string) []string { + // Every updated issue has special label containing number of updates + appendLabelRegex := regexp.MustCompile(`(appended-alerts)::(\d+)`) + alreadyAppended := false + var newLabels []string + for _, l := range labels { + // Check if the label is the special one + matched := appendLabelRegex.FindStringSubmatch(l) + if len(matched) == 3 { + alreadyAppended = true + // Convert it to number if possible otherwise leave the old one as is + count, err := strconv.Atoi(matched[2]) + if err != nil { + level.Error(g.logger).Log("msg", "failed to parse gitlab issue label `appended-alerts`, leaving it unmodified", "label_value", l, "err", err) + newLabels = append(newLabels, l) + continue + } + // Increase the number of appends and add override the old label with it + newLabels = append(newLabels, g.formatGitlabScopedLabel(matched[1], strconv.Itoa(count+1))) + continue + } + newLabels = append(newLabels, l) + } + if !alreadyAppended { + newLabels = append(newLabels, g.formatGitlabScopedLabel("appended-alerts", "1")) + } + return newLabels +} + +func (g *Gitlab) updateGitlabIssue(issue *gitlab.Issue, issueText *bytes.Buffer) error { + newLabels := g.increaseAppendLabel(issue.Labels) + options := &gitlab.UpdateIssueOptions{ + // Concat original description with the new rendered template separated by `Appended on <date>` statement + Description: gitlab.String(fmt.Sprintf("%s\n\n \n\n \n\n \n\n_Appended on `%s`_\n%s", issue.Description, time.Now().Local(), issueText.String())), + Labels: newLabels, + } + issue, response, err := g.client.Issues.UpdateIssue(g.projectId, issue.IID, options) + if err != nil { + metrics.ReportError("FailedToUpdateGitlabIssue", "gitlab") + level.Error(g.logger).Log("msg", "failed to update gitlab issue, will try to create new", "err", err, "response", response) + return err + } + level.Info(g.logger).Log("msg", "updated issue in gitlab", "gitlab_issue_id", issue.IID) + return nil +} + +// CreateIssue from the Webhook in Gitlab +func (g *Gitlab) CreateIssue(msg *alertmanager.Webhook) error { + // Extract grouping labels from the message + groupingLabels := g.extractGroupingLabels(msg) + + // Check for existing issues with same grouping labels + matchingIssues, err := g.getOpenIssuesSince(groupingLabels, g.getTimeBefore(g.groupInterval)) + if err != nil { + level.Warn(g.logger).Log("msg", "listing of open issues to check for duplicates failed , opening a new one even though possible duplicate") + } + + // Try to render the issue text template + issueText, err := g.renderIssueTemplate(msg) + if err != nil { + return err + } + + if len(matchingIssues) > 0 { + // Issues are ordered by created date, we update the first so the newest one. + issueToUpdate := matchingIssues[0] + if err := g.updateGitlabIssue(issueToUpdate, issueText); err != nil { + level.Warn(g.logger).Log("msg", "updating an existing issue failed, opening a new one", "updated_issue_id", issueToUpdate.IID) + } else { + return nil + } + } + // Try to create a new issue rather than discarding it after failed update. + if err := g.createGitlabIssue(msg, groupingLabels, issueText); err != nil { + return err + } + return nil +} + +func (g *Gitlab) ping() error { + level.Debug(g.logger).Log("msg", "trying to ping gitlab", "url", g.client.BaseURL()) + _, err := http.Head(g.client.BaseURL().String()) + if err != nil { + metrics.ReportError("FailedToPingGitlab", "gitlab") + level.Error(g.logger).Log("msg", "failed to ping gitlab with HEAD request", "err", err) + return err + } + return nil +} diff --git a/pkg/handler/handler.go b/pkg/handler/handler.go new file mode 100644 index 0000000000000000000000000000000000000000..2159e7fa20cf61c1b9e9c8aa0f4a22bb69546b9b --- /dev/null +++ b/pkg/handler/handler.go @@ -0,0 +1,72 @@ +// Copyright 2019 FUSAKLA Martin Chodúr +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package handler + +import ( + "net/http" + "strconv" + "time" + + "github.com/fusakla/prometheus-gitlab-notifier/pkg/metrics" + "github.com/go-kit/kit/log" + "github.com/go-kit/kit/log/level" + "github.com/prometheus/client_golang/prometheus" +) + +var ( + requestDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{ + Name: "request_duration_seconds", + Help: "Time (in seconds) spent serving HTTP requests.", + Buckets: prometheus.DefBuckets, + }, []string{"app", "method", "endpoint", "status_code"}) +) + +func init() { + metrics.Register(requestDuration) +} + +type instrumentedWriter struct { + http.ResponseWriter + status int +} + +// WriteHeader writes header to the response. +func (w *instrumentedWriter) WriteHeader(status int) { + w.status = status + w.ResponseWriter.WriteHeader(status) +} + +func (w *instrumentedWriter) Write(b []byte) (int, error) { + if w.status == 0 { + w.status = 200 + } + n, err := w.ResponseWriter.Write(b) + return n, err +} + +// Instrumented returns instrumented handler which provides access logging and prometheus metrics for incoming requests. +func Instrumented(logger log.Logger, handler http.Handler) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + sw := instrumentedWriter{ResponseWriter: w} + handler.ServeHTTP(&sw, r) + duration := time.Since(start) + level.Info(logger).Log("msg", "access log", "uri", r.RequestURI, "method", r.Method, "status", sw.status, "remote_addr", r.RemoteAddr, "duration", duration) + metricsEndpoint := r.URL.Path + if sw.status == 404 { + metricsEndpoint = "non-existing-endpoint" + } + requestDuration.WithLabelValues(metrics.AppLabel, r.Method, metricsEndpoint, strconv.Itoa(sw.status)).Observe(float64(duration.Seconds())) + } +} diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go new file mode 100644 index 0000000000000000000000000000000000000000..668c05cbbe701b75e4a79cdfba280f52b6141a73 --- /dev/null +++ b/pkg/metrics/metrics.go @@ -0,0 +1,73 @@ +// Copyright 2019 FUSAKLA Martin Chodúr +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package metrics + +import ( + "github.com/gorilla/mux" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +// AppLabel is constant name of the application used +const AppLabel = "prometheus-gitlab-notifier" + +var ( + appVersion = "unknown" + gitRevision = "unknown" + gitBranch = "unknown" + gitTag = "unknown" + + registry *prometheus.Registry + errorsTotal *prometheus.CounterVec + appBuildInfo *prometheus.CounterVec +) + +func init() { + registry = prometheus.NewRegistry() + + // Metric with information about build AppVersion, golang AppVersion etc/ + appBuildInfo = prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: "app_build_info", + Help: "Metadata metric with info about build and AppVersion.", + }, []string{"app", "version", "revision", "branch", "tag"}) + registry.MustRegister(appBuildInfo) + appBuildInfo.WithLabelValues(AppLabel, appVersion, gitRevision, gitBranch, gitTag).Inc() + + // Generic metric for reporting errors + errorsTotal = prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: "errors_total", + Help: "Count of occurred errors.", + }, []string{"app", "type", "remote_app"}) + registry.MustRegister(errorsTotal) + + // When using custom registry we need to explicitly register the Go and process collectors. + registry.MustRegister(prometheus.NewGoCollector()) + registry.MustRegister(prometheus.NewProcessCollector(prometheus.ProcessCollectorOpts{})) + +} + +// HandleInRouter registers prometheus metrics rendering in given router. +func HandleInRouter(r *mux.Router) { + r.Handle("/metrics", promhttp.HandlerFor(registry, promhttp.HandlerOpts{})) +} + +// Register new Prometheus metric Collector to the registry. +func Register(m prometheus.Collector) { + registry.MustRegister(m) +} + +// ReportError to errors_total metric global for the whole application. +func ReportError(errorType string, remoteApp string) { + errorsTotal.WithLabelValues(AppLabel, errorType, remoteApp).Inc() +} diff --git a/pkg/prober/prober.go b/pkg/prober/prober.go new file mode 100644 index 0000000000000000000000000000000000000000..823a7d777dbdf75152f6b99f66e174ab18b9e829 --- /dev/null +++ b/pkg/prober/prober.go @@ -0,0 +1,79 @@ +// Copyright 2019 FUSAKLA Martin Chodúr +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package prober + +import ( + "io" + "net/http" + "sync" + + "github.com/go-kit/kit/log" + "github.com/go-kit/kit/log/level" + "github.com/gorilla/mux" +) + +// NewInRouter returns new Prober which registers it's endpoints in the Router to provide readiness and liveness endpoints. +func NewInRouter(logger log.Logger, router *mux.Router) *prober { + p := &prober{ + logger: logger, + serverReady: nil, + } + p.registerInRouter(router) + return p +} + +// prober holds application readiness/liveness status and provides handlers for reporting it. +type prober struct { + logger log.Logger + serverReadyMtx sync.RWMutex + serverReady error +} + +func (p *prober) registerInRouter(router *mux.Router) { + router.HandleFunc("/liveness", p.livenessHandler) + router.HandleFunc("/readiness", p.readinessHandler) +} + +func (p *prober) livenessHandler(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = io.WriteString(w, `OK`) +} + +func (p *prober) writeFailedReadiness(w http.ResponseWriter, err error) { + level.Error(p.logger).Log("msg", "readiness probe failed", "err", err) + http.Error(w, err.Error(), http.StatusServiceUnavailable) +} + +func (p *prober) readinessHandler(w http.ResponseWriter, r *http.Request) { + if err := p.isReady(); err != nil { + p.writeFailedReadiness(w, err) + return + } + w.WriteHeader(http.StatusOK) + _, _ = io.WriteString(w, `OK`) +} + +// SetServerNotReady sets the readiness probe to invalid state. +func (p *prober) SetServerNotReady(err error) { + p.serverReadyMtx.Lock() + defer p.serverReadyMtx.Unlock() + level.Warn(p.logger).Log("msg", "Marking server as not ready", "reason", err) + p.serverReady = err +} + +func (p *prober) isReady() error { + p.serverReadyMtx.RLock() + defer p.serverReadyMtx.RUnlock() + return p.serverReady +} diff --git a/pkg/processor/processor.go b/pkg/processor/processor.go new file mode 100644 index 0000000000000000000000000000000000000000..815852df0a42c5fa21450b6dd5598f412f3f9d16 --- /dev/null +++ b/pkg/processor/processor.go @@ -0,0 +1,85 @@ +// Copyright 2019 FUSAKLA Martin Chodúr +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package processor + +import ( + "context" + "time" + + "github.com/fusakla/prometheus-gitlab-notifier/pkg/alertmanager" + "github.com/fusakla/prometheus-gitlab-notifier/pkg/gitlab" + "github.com/go-kit/kit/log" + "github.com/go-kit/kit/log/level" + "github.com/prometheus/client_golang/prometheus" +) + +var ( + processedItems = prometheus.NewCounter(prometheus.CounterOpts{ + Name: "prometheus_gitlab_notifier_processed_alerts_processed_total", + Help: "Count of processed alerts.", + }) + retryCount = prometheus.NewCounter(prometheus.CounterOpts{ + Name: "prometheus_gitlab_notifier_processed_alerts_retried_total", + Help: "Count of retries.", + }) +) + +func init() { + prometheus.MustRegister(processedItems) + prometheus.MustRegister(retryCount) +} + +// New returns new processor which handles the alert queue and retrying. +func New(logger log.Logger) *processor { + return &processor{ + logger: logger, + } +} + +type processor struct { + logger log.Logger +} + +// Process processes alerts from the given channel and creates Gitlab issues from them. +func (p *processor) Process(ctx context.Context, gitlab *gitlab.Gitlab, alertChannel chan *alertmanager.Webhook, retryLimit int, retryBackoff time.Duration) { + doneChannel := make(chan bool, 1) + go func() { + defer close(doneChannel) + for { + select { + case <-ctx.Done(): + return + case alert, ok := <-alertChannel: + if !ok { + return + } + level.Debug(p.logger).Log("msg", "fetched alert from queue for processing", "group_key", alert.GroupKey) + if err := gitlab.CreateIssue(alert); err != nil { + if alert.RetryCount() >= retryLimit-1 { + level.Warn(p.logger).Log("msg", "alert exceeded maximum number of retries, dropping it", "group_key", alert.GroupKey, "retry_count", retryLimit) + continue + } + go func() { + time.Sleep(retryBackoff) + alert.Retry() + alertChannel <- alert + retryCount.Inc() + level.Warn(p.logger).Log("msg", "added alert to queue for retrying ", "group_key", alert.GroupKey, "retry_backoff", retryBackoff) + }() + } + processedItems.Inc() + } + } + }() +}