pax_global_header00006660000000000000000000000064134167455220014523gustar00rootroot0000000000000052 comment=93f117e1b7e487cf1f5da30a28cd554e33614909 prometheus-alertmanager-0.15.3+ds/000077500000000000000000000000001341674552200170465ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/.circleci/000077500000000000000000000000001341674552200207015ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/.circleci/config.yml000066400000000000000000000065751341674552200227060ustar00rootroot00000000000000--- version: 2 jobs: test: docker: - image: circleci/golang:1.10 working_directory: /go/src/github.com/prometheus/alertmanager steps: - checkout - setup_remote_docker - run: make promu - run: make - run: rm -v alertmanager amtool build: machine: true working_directory: /home/circleci/.go_workspace/src/github.com/prometheus/alertmanager steps: - checkout - run: make promu - run: promu crossbuild -v - persist_to_workspace: root: . paths: - .build docker_hub_master: docker: - image: circleci/golang:1.10 working_directory: /go/src/github.com/prometheus/alertmanager environment: DOCKER_IMAGE_NAME: prom/alertmanager QUAY_IMAGE_NAME: quay.io/prometheus/alertmanager steps: - checkout - setup_remote_docker - attach_workspace: at: . - run: ln -s .build/linux-amd64/alertmanager alertmanager - run: ln -s .build/linux-amd64/amtool amtool - run: make docker DOCKER_IMAGE_NAME=$DOCKER_IMAGE_NAME - run: make docker DOCKER_IMAGE_NAME=$QUAY_IMAGE_NAME - run: docker images - run: docker login -u $DOCKER_LOGIN -p $DOCKER_PASSWORD - run: docker login -u $QUAY_LOGIN -p $QUAY_PASSWORD quay.io - run: docker push $DOCKER_IMAGE_NAME - run: docker push $QUAY_IMAGE_NAME docker_hub_release_tags: docker: - image: circleci/golang:1.10 working_directory: /go/src/github.com/prometheus/alertmanager environment: DOCKER_IMAGE_NAME: prom/alertmanager QUAY_IMAGE_NAME: quay.io/prometheus/alertmanager steps: - checkout - setup_remote_docker - run: mkdir -v -p ${HOME}/bin - run: curl -L 'https://github.com/aktau/github-release/releases/download/v0.7.2/linux-amd64-github-release.tar.bz2' | tar xvjf - --strip-components 3 -C ${HOME}/bin - run: echo 'export PATH=${HOME}/bin:${PATH}' >> ${BASH_ENV} - attach_workspace: at: . - run: make promu - run: promu crossbuild tarballs - run: promu checksum .tarballs - run: promu release .tarballs - store_artifacts: path: .tarballs destination: releases - run: ln -s .build/linux-amd64/alertmanager alertmanager - run: ln -s .build/linux-amd64/amtool amtool - run: make docker DOCKER_IMAGE_NAME=$DOCKER_IMAGE_NAME DOCKER_IMAGE_TAG=$CIRCLE_TAG - run: make docker DOCKER_IMAGE_NAME=$QUAY_IMAGE_NAME DOCKER_IMAGE_TAG=$CIRCLE_TAG - run: docker login -u $DOCKER_LOGIN -p $DOCKER_PASSWORD - run: docker login -u $QUAY_LOGIN -p $QUAY_PASSWORD quay.io - run: | if [[ "$CIRCLE_TAG" =~ ^v[0-9]+(\.[0-9]+){2}$ ]]; then docker tag "$DOCKER_IMAGE_NAME:$CIRCLE_TAG" "$DOCKER_IMAGE_NAME:latest" docker tag "$QUAY_IMAGE_NAME:$CIRCLE_TAG" "$QUAY_IMAGE_NAME:latest" fi - run: docker push $DOCKER_IMAGE_NAME - run: docker push $QUAY_IMAGE_NAME workflows: version: 2 alertmanager: jobs: - test: filters: tags: only: /.*/ - build: filters: tags: only: /.*/ - docker_hub_master: requires: - test - build filters: branches: only: master - docker_hub_release_tags: requires: - test - build filters: tags: only: /^v[0-9]+(\.[0-9]+){2}(-.+|[^-.]*)$/ branches: ignore: /.*/ prometheus-alertmanager-0.15.3+ds/.dockerignore000066400000000000000000000000511341674552200215160ustar00rootroot00000000000000.build/ .tarballs/ !.build/linux-amd64/ prometheus-alertmanager-0.15.3+ds/.github/000077500000000000000000000000001341674552200204065ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/.github/ISSUE_TEMPLATE.md000066400000000000000000000017061341674552200231170ustar00rootroot00000000000000 **What did you do?** **What did you expect to see?** **What did you see instead? Under which circumstances?** **Environment** * System information: insert output of `uname -srm` here * Alertmanager version: insert output of `alertmanager --version` here * Prometheus version: insert output of `prometheus -version` here (if relevant to the issue) * Alertmanager configuration file: ``` insert configuration here ``` * Prometheus configuration file: ``` insert configuration here (if relevant to the issue) ``` * Logs: ``` insert Prometheus and Alertmanager logs relevant to the issue here ``` prometheus-alertmanager-0.15.3+ds/.gitignore000066400000000000000000000002731341674552200210400ustar00rootroot00000000000000/data/ /alertmanager /amtool *.yml *.yaml /.build /.release /.tarballs !/cli/testdata/*.yml !/cli/config/testdata/*.yml !/doc/examples/simple.yml !/circle.yml !/.travis.yml !/.promu.yml prometheus-alertmanager-0.15.3+ds/.promu.yml000066400000000000000000000024231341674552200210120ustar00rootroot00000000000000repository: path: github.com/prometheus/alertmanager build: binaries: - name: alertmanager path: ./cmd/alertmanager - name: amtool path: ./cmd/amtool flags: -a -tags netgo ldflags: | -X {{repoPath}}/vendor/github.com/prometheus/common/version.Version={{.Version}} -X {{repoPath}}/vendor/github.com/prometheus/common/version.Revision={{.Revision}} -X {{repoPath}}/vendor/github.com/prometheus/common/version.Branch={{.Branch}} -X {{repoPath}}/vendor/github.com/prometheus/common/version.BuildUser={{user}}@{{host}} -X {{repoPath}}/vendor/github.com/prometheus/common/version.BuildDate={{date "20060102-15:04:05"}} tarball: files: - examples/ha/alertmanager.yml - LICENSE - NOTICE crossbuild: platforms: - linux/amd64 - linux/386 - darwin/amd64 - darwin/386 - windows/amd64 - windows/386 - freebsd/amd64 - freebsd/386 - openbsd/amd64 - openbsd/386 - netbsd/amd64 - netbsd/386 - dragonfly/amd64 - linux/arm - linux/arm64 - freebsd/arm - netbsd/arm - linux/ppc64 - linux/ppc64le - linux/mips64 - linux/mips64le prometheus-alertmanager-0.15.3+ds/.travis.yml000066400000000000000000000002771341674552200211650ustar00rootroot00000000000000sudo: false language: go services: - docker go: - 1.10.x script: # test front-end - make clean - cd ui/app/ && make && cd ../.. - make assets - git diff --exit-code # test back-end - make prometheus-alertmanager-0.15.3+ds/CHANGELOG.md000066400000000000000000000446421341674552200206710ustar00rootroot00000000000000## 0.15.3 / 2018-11-09 * [BUGFIX] Fix alert merging supporting both empty and set EndsAt property for firing alerts send by Prometheus (#1611) ## 0.15.2 / 2018-08-14 * [ENHANCEMENT] [amtool] Add support for stdin to check-config (#1431) * [ENHANCEMENT] Log PagerDuty v1 response on BadRequest (#1481) * [BUGFIX] Correctly encode query strings in notifiers (#1516) * [BUGFIX] Add cache control headers to the API responses to avoid IE caching (#1500) * [BUGFIX] Avoid listener blocking on unsubscribe (#1482) * [BUGFIX] Fix a bunch of unhandled errors (#1501) * [BUGFIX] Update PagerDuty API V2 to send full details on resolve (#1483) * [BUGFIX] Validate URLs at config load time (#1468) * [BUGFIX] Fix Settle() interval (#1478) * [BUGFIX] Fix email to be green if only none firing (#1475) * [BUGFIX] Handle errors in notify (#1474) * [BUGFIX] Fix templating of hipchat room id (#1463) ## 0.15.1 / 2018-07-10 * [BUGFIX] Fix email template typo in alert-warning style (#1421) * [BUGFIX] Fix regression in Pager Duty config (#1455) * [BUGFIX] Catch templating errors in Wechat Notify (#1436) * [BUGFIX] Fail when no private address can be found for cluster (#1437) * [BUGFIX] Make sure we don't miss the first pushPull when joining cluster (#1456) * [BUGFIX] Fix concurrent read and wirte group error in dispatch (#1447) ## 0.15.0 / 2018-06-22 * [CHANGE] [amtool] Update silence add and update flags (#1298) * [CHANGE] Replace deprecated InstrumentHandler() (#1302) * [CHANGE] Validate Slack field config and only allow the necessary input (#1334) * [CHANGE] Remove legacy alert ingest endpoint (#1362) * [CHANGE] Move to memberlist as underlying gossip protocol including cluster flag changes from --mesh.xxx to --cluster.xxx (#1232) * [CHANGE] Move Alertmanager working directory in Docker image to /etc/alertmanager (#1313) * [BUGFIX/CHANGE] The default group by is no labels. (#1287) * [FEATURE] [amtool] Filter alerts by receiver (#1402) * [FEATURE] Wait for mesh to settle before sending alerts (#1209) * [FEATURE] [amtool] Support basic auth in alertmanager url (#1279) * [FEATURE] Make HTTP clients used for integrations configurable * [ENHANCEMENT] Support receiving alerts with end time and zero start time * [ENHANCEMENT] Sort dispatched alerts by job+instance (#1234) * [ENHANCEMENT] Support alert query filters `active` and `unprocessed` (#1366) * [ENHANCEMENT] [amtool] Expose alert query flags --active and --unprocessed (#1370) * [ENHANCEMENT] Add Slack actions to notifications (#1355) * [BUGFIX] Register nflog snapShotSize metric * [BUGFIX] Sort alerts in correct order before flushing to notifiers (#1349) * [BUGFIX] Don't reset initial wait timer if flush is in-progress (#1301) * [BUGFIX] Fix resolved alerts still inhibiting (#1331) * [BUGFIX] Template wechat config fields (#1356) * [BUGFIX] Notify resolved alerts properly (#1408) * [BUGFIX] Fix parsing for label values with commas (#1395) * [BUGFIX] Hide sensitive Wechat configuration (#1253) * [BUGFIX] Prepopulate matchers when recreating a silence (#1270) * [BUGFIX] Fix wechat panic (#1293) * [BUGFIX] Allow empty matchers in silences/filtering (#1289) * [BUGFIX] Properly configure HTTP client for Wechat integration ## 0.14.0 / 2018-02-12 * [ENHANCEMENT] [amtool] Silence update support dwy suffixes to expire flag (#1197) * [ENHANCEMENT] Allow templating PagerDuty receiver severity (#1214) * [ENHANCEMENT] Include receiver name in failed notifications log messages (#1207) * [ENHANCEMENT] Allow global opsgenie api key (#1208) * [ENHANCEMENT] Add mesh metrics (#1225) * [ENHANCEMENT] Add Class field to PagerDuty; add templating to PagerDuty-CEF fields (#1231) * [BUGFIX] Don't notify of resolved alerts if none were reported firing (#1198) * [BUGFIX] Notify only when new firing alerts are added (#1205) * [BUGFIX] [mesh] Fix pending connections never set to established (#1204) * [BUGFIX] Allow OpsGenie notifier to have empty team fields (#1224) * [BUGFIX] Don't count alerts with EndTime in the future as resolved (#1233) * [BUGFIX] Speed up re-rendering of Silence UI (#1235) * [BUGFIX] Forbid 0 value for group_interval and repeat_interval (#1230) * [BUGFIX] Fix WeChat agentid issue (#1229) ## 0.13.0 / 2018-01-12 * [CHANGE] Switch cmd/alertmanager to kingpin (#974) * [CHANGE] [amtool] Switch amtool to kingpin (#976) * [CHANGE] [amtool] silence query: --expired flag only shows expired silences (#1190) * [CHANGE] Return config reload result from reload endpoint (#1180) * [FEATURE] UI silence form is populated from location bar (#1148) * [FEATURE] Add /-/healthy endpoint (#1159) * [ENHANCEMENT] Instrument and log snapshot sizes on maintenance (#1155) * [ENHANCEMENT] Make alertGC interval configurable (#1151) * [ENHANCEMENT] Display mesh connections in the Status page (#1164) * [BUGFIX] Template service keys for pagerduty notifier (#1182) * [BUGFIX] Fix expire buttons on the silences page (#1171) * [BUGFIX] Fix JavaScript error in MSIE due to endswith() usage (#1172) * [BUGFIX] Correctly format UI error output (#1167) ## 0.12.0 / 2017-12-15 * [FEATURE] package amtool in docker container (#1127) * [FEATURE] Add notify support for Chinese User wechat (#1059) * [FEATURE] [amtool] Add a new `silence import` command (#1082) * [FEATURE] [amtool] Add new command to update silence (#1123) * [FEATURE] [amtool] Add ability to query for silences that will expire soon (#1120) * [ENHANCEMENT] Template source field in PagerDuty alert payload (#1117) * [ENHANCEMENT] Add footer field for slack messages (#1141) * [ENHANCEMENT] Add Slack additional "fields" to notifications (#1135) * [ENHANCEMENT] Adding check for webhook's URL formatting (#1129) * [ENHANCEMENT] Let the browser remember the creator of a silence (#1112) * [BUGFIX] Fix race in stopping inhibitor (#1118) * [BUGFIX] Fix browser UI when entering negative duration (#1132) ## 0.11.0 / 2017-11-16 * [CHANGE] Make silence negative filtering consistent with alert filtering (#1095) * [CHANGE] Change HipChat and OpsGenie api config names (#1087) * [ENHANCEMENT] amtool: Allow 'd', 'w', 'y' time suffixes when creating silence (#1091) * [ENHANCEMENT] Support OpsGenie Priority field (#1094) * [BUGFIX] Fix UI when no silences are present (#1090) * [BUGFIX] Fix OpsGenie Teams field (#1101) * [BUGFIX] Fix OpsGenie Tags field (#1108) ## 0.10.0 / 2017-11-09 * [CHANGE] Prevent inhibiting alerts in the source of the inhibition (#1017) * [ENHANCEMENT] Improve amtool check-config use and description text (#1016) * [ENHANCEMENT] Add metrics about current silences and alerts (#998) * [ENHANCEMENT] Sorted silences based on current status (#1015) * [ENHANCEMENT] Add metric of alertmanager position in mesh (#1024) * [ENHANCEMENT] Initialise notifications_total and notifications_failed_total (#1011) * [ENHANCEMENT] Allow selectable matchers on silence view (#1030) * [ENHANCEMENT] Allow template in victorops message_type field (#1038) * [ENHANCEMENT] Optionally hide inhibited alerts in API response (#1039) * [ENHANCEMENT] Toggle silenced and inhibited alerts in UI (#1049) * [ENHANCEMENT] Fix pushover limits (title, message, url) (#1055) * [ENHANCEMENT] Add limit to OpsGenie message (#1045) * [ENHANCEMENT] Upgrade OpsGenie notifier to v2 API. (#1061) * [ENHANCEMENT] Allow template in victorops routing_key field (#1083) * [ENHANCEMENT] Add support for PagerDuty API v2 (#1054) * [BUGFIX] Fix inhibit race (#1032) * [BUGFIX] Fix segfault on amtool (#1031) * [BUGFIX] Remove .WasInhibited and .WasSilenced fields of Alert type (#1026) * [BUGFIX] nflog: Fix Log() crash when gossip is nil (#1064) * [BUGFIX] Fix notifications for flapping alerts (#1071) * [BUGFIX] Fix shutdown crash with nil mesh router (#1077) * [BUGFIX] Fix negative matchers filtering (#1077) ## 0.9.1 / 2017-09-29 * [BUGFIX] Fix -web.external-url regression in ui (#1008) * [BUGFIX] Fix multipart email implementation (#1009) ## 0.9.0 / 2017-09-28 * [ENHANCEMENT] Add current time to webhook message (#909) * [ENHANCEMENT] Add link_names to slack notifier (#912) * [ENHANCEMENT] Make ui labels selectable/highlightable (#932) * [ENHANCEMENT] Make links in ui annotations selectable (#946) * [ENHANCEMENT] Expose the alert's "fingerprint" (unique identifier) through API (#786) * [ENHANCEMENT] Add README information for amtool (#939) * [ENHANCEMENT] Use user-set logging option consistently throughout alertmanager (#968) * [ENHANCEMENT] Sort alerts returned from API by their fingerprint (#969) * [ENHANCEMENT] Add edit/delete silence buttons on silence page view (#970) * [ENHANCEMENT] Add check-config subcommand to amtool (#978) * [ENHANCEMENT] Add email notification text content support (#934) * [ENHANCEMENT] Support passing binary name to make build target (#990) * [ENHANCEMENT] Show total no. of silenced alerts in preview (#994) * [ENHANCEMENT] Added confirmation dialog when expiring silences (#993) * [BUGFIX] Fix crash when no mesh router is configured (#919) * [BUGFIX] Render status page without mesh (#920) * [BUGFIX] Exit amtool subcommands with non-zero error code (#938) * [BUGFIX] Change mktemp invocation in makefile to work for macOS (#971) * [BUGFIX] Add a mutex to silences.go:gossipData (#984) * [BUGFIX] silences: avoid deadlock (#995) * [BUGFIX] Ignore expired silences OnGossip (#999) ## 0.8.0 / 2017-07-20 * [FEATURE] Add ability to filter alerts by receiver in the UI (#890) * [FEATURE] Add User-Agent for webhook requests (#893) * [ENHANCEMENT] Add possibility to have a global victorops api_key (#897) * [ENHANCEMENT] Add EntityDisplayName and improve StateMessage for Victorops (#769) * [ENHANCEMENT] Omit empty config fields and show regex upon re-marshalling to elide secrets (#864) * [ENHANCEMENT] Parse API error messages in UI (#866) * [ENHANCEMENT] Enable sending mail via smtp port 465 (#704) * [BUGFIX] Prevent duplicate notifications by sorting matchers (#882) * [BUGFIX] Remove timeout for UI requests (#890) * [BUGFIX] Update config file location of CLI in flag usage text (#895) ## 0.7.1 / 2017-06-09 * [BUGFIX] Fix filtering by label on Alert list and Silence list page ## 0.7.0 / 2017-06-08 * [CHANGE] Rewrite UI from scratch improving UX * [CHANGE] Rename `config` to `configYAML` on `api/v1/status` * [FEATURE] Add ability to update a silence on `api/v1/silences` POST endpoint (See #765) * [FEATURE] Return alert status on `api/v1/alerts` GET endpoint * [FEATURE] Serve silence state on `api/v1/silences` GET endpoint * [FEATURE] Add ability to specify a route prefix * [FEATURE] Add option to disable AM listening on mesh port * [ENHANCEMENT] Add ability to specify `filter` string and `silenced` flag on `api/v1/alerts` GET endpoint * [ENHANCEMENT] Update `cache-control` to prevent caching for web assets in general. * [ENHANCEMENT] Serve web assets by alertmanager instead of external CDN (See #846) * [ENHANCEMENT] Elide secrets in alertmanager config (See #840) * [ENHANCEMENT] AMTool: Move config file to a more consistent location (See #843) * [BUGFIX] Enable builds for Solaris/Illumos * [BUGFIX] Load web assets based on url path (See #323) ## 0.6.2 / 2017-05-09 * [BUGFIX] Correctly link to silences from alert again * [BUGFIX] Correctly hide silenced/show active alerts in UI again * [BUGFIX] Fix regression of alerts not being displayed until first processing * [BUGFIX] Fix internal usage of wrong lock for silence markers * [BUGFIX] Adapt amtool's API parsing to recent API changes * [BUGFIX] Correctly marshal regexes in config JSON response * [CHANGE] Anchor silence regex matchers to be consistent with Prometheus * [ENHANCEMENT] Error if root route is using `continue` keyword ## 0.6.1 / 2017-04-28 * [BUGFIX] Fix incorrectly serialized hash for notification providers. * [ENHANCEMENT] Add processing status field to alerts. * [FEATURE] Add config hash metric. ## 0.6.0 / 2017-04-25 * [BUGFIX] Add `groupKey` to `alerts/groups` endpoint https://github.com/prometheus/alertmanager/pull/576 * [BUGFIX] Only notify on firing alerts https://github.com/prometheus/alertmanager/pull/595 * [BUGFIX] Correctly marshal regex's in config for routing tree https://github.com/prometheus/alertmanager/pull/602 * [BUGFIX] Prevent panic when failing to load config https://github.com/prometheus/alertmanager/pull/607 * [BUGFIX] Prevent panic when alertmanager is started with an empty `-mesh.peer` https://github.com/prometheus/alertmanager/pull/726 * [CHANGE] Rename VictorOps config variables https://github.com/prometheus/alertmanager/pull/667 * [CHANGE] No longer generate releases for openbsd/arm https://github.com/prometheus/alertmanager/pull/732 * [ENHANCEMENT] Add `DELETE` as accepted CORS method https://github.com/prometheus/alertmanager/commit/0ecc59076ca6b4cbb63252fa7720a3d89d1c81d3 * [ENHANCEMENT] Switch to using `gogoproto` for protobuf https://github.com/prometheus/alertmanager/pull/715 * [ENHANCEMENT] Include notifier type in logs and errors https://github.com/prometheus/alertmanager/pull/702 * [FEATURE] Expose mesh peers on status page https://github.com/prometheus/alertmanager/pull/644 * [FEATURE] Add `reReplaceAll` template function https://github.com/prometheus/alertmanager/pull/639 * [FEATURE] Allow label-based filtering alerts/silences through API https://github.com/prometheus/alertmanager/pull/633 * [FEATURE] Add commandline tool for interacting with alertmanager https://github.com/prometheus/alertmanager/pull/636 ## 0.5.1 / 2016-11-24 * [BUGFIX] Fix crash caused by race condition in silencing * [ENHANCEMENT] Improve logging of API errors * [ENHANCEMENT] Add metrics for the notification log ## 0.5.0 / 2016-11-01 This release requires a storage wipe. It contains fundamental internal changes that came with implementing the high availability mode. * [FEATURE] Alertmanager clustering for high availability * [FEATURE] Garbage collection of old silences and notification logs * [CHANGE] New storage format * [CHANGE] Stricter silence semantics for consistent historical view ## 0.4.2 / 2016-09-02 * [BUGFIX] Fix broken regex checkbox in silence form * [BUGFIX] Simplify inconsistent silence update behavior ## 0.4.1 / 2016-08-31 * [BUGFIX] Wait for silence query to finish instead of showing error * [BUGFIX] Fix sorting of silences * [BUGFIX] Provide visual feedback after creating a silence * [BUGFIX] Fix styling of silences * [ENHANCEMENT] Provide cleaner API silence interface ## 0.4.0 / 2016-08-23 * [FEATURE] Silences are now paginated in the web ui * [CHANGE] Failure to start on unparsed flags ## 0.3.0 / 2016-07-07 * [CHANGE] Alerts are purely in memory and no longer persistent across restarts * [FEATURE] Add SMTP LOGIN authentication mechanism ## 0.2.1 / 2016-06-23 * [ENHANCEMENT] Allow inheritance of route receiver * [ENHANCEMENT] Add silence cache to silence provider * [BUGFIX] Fix HipChat room number in integration URL ## 0.2.0 / 2016-06-17 This release uses a new storage backend based on BoltDB. You have to backup and wipe your former storage path to run it. * [CHANGE] Use BoltDB as data store. * [CHANGE] Move SMTP authentification to configuration file * [FEATURE] add /-/reload HTTP endpoint * [FEATURE] Filter silenced alerts in web UI * [ENHANCEMENT] reduce inhibition computation complexity * [ENHANCEMENT] Add support for teams and tags in OpsGenie integration * [BUGFIX] Handle OpsGenie responses correctly * [BUGFIX] Fix Pushover queue length issue * [BUGFIX] STARTTLS before querying auth mechanism in email integration ## 0.1.1 / 2016-03-15 * [BUGFIX] Fix global database lock issue * [ENHANCEMENT] Improve SQLite alerts index * [ENHANCEMENT] Enable debug endpoint ## 0.1.0 / 2016-02-23 This version is a full rewrite of the Alertmanager with a very different feature set. Thus, there is no meaningful changelog. Changes with respect to 0.1.0-beta2: * [CHANGE] Expose same data structure to templates and webhook * [ENHANCEMENT] Show generator URL in default templates and web UI * [ENHANCEMENT] Support for Slack icon_emoji field * [ENHANCEMENT] Expose incident key to templates and webhook data * [ENHANCEMENT] Allow markdown in Slack 'text' field * [BUGFIX] Fixed database locking issue ## 0.1.0-beta2 / 2016-02-03 * [BUGFIX] Properly set timeout for incoming alerts with fixed start time * [ENHANCEMENT] Send source field in OpsGenie integration * [ENHANCEMENT] Improved routing configuration validation * [FEATURE] Basic instrumentation added ## 0.1.0-beta1 / 2016-01-08 * [BUGFIX] Send full alert group state on each update. Fixes erroneous resolved notifications. * [FEATURE] HipChat integration * [CHANGE] Slack integration no longer sends resolved notifications by default ## 0.1.0-beta0 / 2015-12-23 This version is a full rewrite of the Alertmanager with a very different feature set. Thus, there is no meaningful changelog. ## 0.0.4 / 2015-09-09 * [BUGFIX] Fix version info string in startup message. * [BUGFIX] Fix Pushover notifications by setting the right priority level, as well as required retry and expiry intervals. * [FEATURE] Make it possible to link to individual alerts in the UI. * [FEATURE] Rearrange alert columns in UI and allow expanding more alert details. * [FEATURE] Add Amazon SNS notifications. * [FEATURE] Add OpsGenie Webhook notifications. * [FEATURE] Add `-web.external-url` flag to control the externally visible Alertmanager URL. * [FEATURE] Add runbook and alertmanager URLs to PagerDuty and email notifications. * [FEATURE] Add a GET API to /api/alerts which pulls JSON formatted AlertAggregates. * [ENHANCEMENT] Sort alerts consistently in web UI. * [ENHANCEMENT] Suggest to use email address as silence creator. * [ENHANCEMENT] Make Slack timeout configurable. * [ENHANCEMENT] Add channel name to error logging about Slack notifications. * [ENHANCEMENT] Refactoring and tests for Flowdock notifications. * [ENHANCEMENT] New Dockerfile using alpine-golang-make-onbuild base image. * [CLEANUP] Add Docker instructions and other cleanups in README.md. * [CLEANUP] Update Makefile.COMMON from prometheus/utils. ## 0.0.3 / 2015-06-10 * [BUGFIX] Fix email template body writer being called with parameters in wrong order. ## 0.0.2 / 2015-06-09 * [BUGFIX] Fixed silences.json permissions in Docker image. * [CHANGE] Changed case of API JSON properties to initial lower letter. * [CHANGE] Migrated logging to use http://github.com/prometheus/log. * [FEATURE] Flowdock notification support. * [FEATURE] Slack notification support. * [FEATURE] Generic webhook notification support. * [FEATURE] Support for "@"-mentions in HipChat notifications. * [FEATURE] Path prefix option to support reverse proxies. * [ENHANCEMENT] Improved web redirection and 404 behavior. * [CLEANUP] Updated compiled web assets from source. * [CLEANUP] Updated fsnotify package to its new source location. * [CLEANUP] Updates to README.md and AUTHORS.md. * [CLEANUP] Various smaller cleanups and improvements. prometheus-alertmanager-0.15.3+ds/CONTRIBUTING.md000066400000000000000000000015501341674552200213000ustar00rootroot00000000000000# Contributing Alertmanager uses GitHub to manage reviews of pull requests. * If you have a trivial fix or improvement, go ahead and create a pull request, addressing (with `@...`) the maintainer of this repository (see [MAINTAINERS.md](MAINTAINERS.md)) in the description of the pull request. * If you plan to do something more involved, first discuss your ideas on our [mailing list](https://groups.google.com/forum/?fromgroups#!forum/prometheus-developers). This will avoid unnecessary work and surely give you and us a good deal of inspiration. * Relevant coding style guidelines are the [Go Code Review Comments](https://code.google.com/p/go-wiki/wiki/CodeReviewComments) and the _Formatting and style_ section of Peter Bourgon's [Go: Best Practices for Production Environments](http://peter.bourgon.org/go-in-production/#formatting-and-style). prometheus-alertmanager-0.15.3+ds/Dockerfile000066400000000000000000000006621341674552200210440ustar00rootroot00000000000000FROM prom/busybox:latest MAINTAINER The Prometheus Authors COPY amtool /bin/amtool COPY alertmanager /bin/alertmanager COPY examples/ha/alertmanager.yml /etc/alertmanager/alertmanager.yml EXPOSE 9093 VOLUME [ "/alertmanager" ] WORKDIR /etc/alertmanager ENTRYPOINT [ "/bin/alertmanager" ] CMD [ "--storage.path=/alertmanager" ] prometheus-alertmanager-0.15.3+ds/LICENSE000066400000000000000000000261351341674552200200620ustar00rootroot00000000000000 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. prometheus-alertmanager-0.15.3+ds/MAINTAINERS.md000066400000000000000000000000521341674552200211370ustar00rootroot00000000000000* Stuart Nelson prometheus-alertmanager-0.15.3+ds/Makefile000066400000000000000000000035761341674552200205210ustar00rootroot00000000000000# Copyright 2015 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. include Makefile.common FRONTEND_DIR = $(BIN_DIR)/ui/app DOCKER_IMAGE_NAME ?= alertmanager ifdef DEBUG bindata_flags = -debug endif STATICCHECK_IGNORE = \ github.com/prometheus/alertmanager/notify/notify.go:SA6002 .PHONY: build-all # Will build both the front-end as well as the back-end build-all: assets build assets: go-bindata ui/bindata.go template/internal/deftmpl/bindata.go go-bindata: -@$(GO) get -u github.com/jteeuwen/go-bindata/... template/internal/deftmpl/bindata.go: template/default.tmpl @go-bindata $(bindata_flags) -mode 420 -modtime 1 -pkg deftmpl -o template/internal/deftmpl/bindata.go template/default.tmpl @$(GO) fmt ./template/internal/deftmpl ui/bindata.go: ui/app/script.js ui/app/index.html ui/app/lib # Using "-mode 420" and "-modtime 1" to make assets make target deterministic. # It sets all file permissions and time stamps to 420 and 1 @go-bindata $(bindata_flags) -mode 420 -modtime 1 -pkg ui -o \ ui/bindata.go ui/app/script.js \ ui/app/index.html \ ui/app/favicon.ico \ ui/app/lib/... @$(GO) fmt ./ui ui/app/script.js: $(shell find ui/app/src -iname *.elm) cd $(FRONTEND_DIR) && $(MAKE) script.js .PHONY: proto proto: scripts/genproto.sh .PHONY: clean clean: rm template/internal/deftmpl/bindata.go rm ui/bindata.go cd $(FRONTEND_DIR) && $(MAKE) clean prometheus-alertmanager-0.15.3+ds/Makefile.common000066400000000000000000000062201341674552200217750ustar00rootroot00000000000000# 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))) PROMU := $(FIRST_GOPATH)/bin/promu STATICCHECK := $(FIRST_GOPATH)/bin/staticcheck GOVENDOR := $(FIRST_GOPATH)/bin/govendor pkgs = ./... PREFIX ?= $(shell pwd) BIN_DIR ?= $(shell pwd) DOCKER_IMAGE_TAG ?= $(subst /,-,$(shell git rev-parse --abbrev-ref HEAD)) .PHONY: all all: style staticcheck unused build test .PHONY: style style: @echo ">> checking code style" ! $(GOFMT) -d $$(find . -path ./vendor -prune -o -name '*.go' -print) | grep '^' .PHONY: check_license 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: test-short test-short: @echo ">> running short tests" $(GO) test -short $(pkgs) .PHONY: test test: @echo ">> running all tests" $(GO) test -race $(pkgs) .PHONY: format format: @echo ">> formatting code" $(GO) fmt $(pkgs) .PHONY: vet vet: @echo ">> vetting code" $(GO) vet $(pkgs) .PHONY: staticcheck staticcheck: $(STATICCHECK) @echo ">> running staticcheck" $(STATICCHECK) -ignore "$(STATICCHECK_IGNORE)" $(pkgs) .PHONY: unused unused: $(GOVENDOR) @echo ">> running check for unused packages" @$(GOVENDOR) list +unused | grep . && exit 1 || echo 'No unused packages' .PHONY: build build: promu @echo ">> building binaries" $(PROMU) build --prefix $(PREFIX) .PHONY: tarball tarball: promu @echo ">> building release tarball" $(PROMU) tarball --prefix $(PREFIX) $(BIN_DIR) .PHONY: docker docker: docker build -t "$(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG)" . .PHONY: promu promu: GOOS= GOARCH= $(GO) get -u github.com/prometheus/promu .PHONY: $(STATICCHECK) $(STATICCHECK): GOOS= GOARCH= $(GO) get -u honnef.co/go/tools/cmd/staticcheck .PHONY: $(GOVENDOR) $(GOVENDOR): GOOS= GOARCH= $(GO) get -u github.com/kardianos/govendor prometheus-alertmanager-0.15.3+ds/NOTICE000066400000000000000000000007111341674552200177510ustar00rootroot00000000000000Prometheus Alertmanager Copyright 2013-2015 The Prometheus Authors This product includes software developed at SoundCloud Ltd. (http://soundcloud.com/). The following components are included in this product: Bootstrap http://getbootstrap.com Copyright 2011-2014 Twitter, Inc. Licensed under the MIT License bootstrap-datetimepicker.js http://www.eyecon.ro/bootstrap-datepicker Copyright 2012 Stefan Petre Licensed under the Apache License, Version 2.0 prometheus-alertmanager-0.15.3+ds/Procfile000066400000000000000000000011551341674552200205360ustar00rootroot00000000000000a1: ./alertmanager --log.level=debug --storage.path=$TMPDIR/a1 --web.listen-address=:9093 --cluster.listen-address=127.0.0.1:8001 --config.file=examples/ha/alertmanager.yml a2: ./alertmanager --log.level=debug --storage.path=$TMPDIR/a2 --web.listen-address=:9094 --cluster.listen-address=127.0.0.1:8002 --cluster.peer=127.0.0.1:8001 --config.file=examples/ha/alertmanager.yml a3: ./alertmanager --log.level=debug --storage.path=$TMPDIR/a3 --web.listen-address=:9095 --cluster.listen-address=127.0.0.1:8003 --cluster.peer=127.0.0.1:8001 --config.file=examples/ha/alertmanager.yml wh: go run ./examples/webhook/echo.go prometheus-alertmanager-0.15.3+ds/README.md000066400000000000000000000314371341674552200203350ustar00rootroot00000000000000# Alertmanager [![Build Status](https://travis-ci.org/prometheus/alertmanager.svg?branch=master)][travis] [![CircleCI](https://circleci.com/gh/prometheus/alertmanager/tree/master.svg?style=shield)][circleci] [![Docker Repository on Quay](https://quay.io/repository/prometheus/alertmanager/status)][quay] [![Docker Pulls](https://img.shields.io/docker/pulls/prom/alertmanager.svg?maxAge=604800)][hub] The Alertmanager handles alerts sent by client applications such as the Prometheus server. It takes care of deduplicating, grouping, and routing them to the correct receiver integrations such as email, PagerDuty, or OpsGenie. It also takes care of silencing and inhibition of alerts. * [Documentation](http://prometheus.io/docs/alerting/alertmanager/) ## Install There are various ways of installing Alertmanager. ### Precompiled binaries Precompiled binaries for released versions are available in the [*download* section](https://prometheus.io/download/) on [prometheus.io](https://prometheus.io). Using the latest production release binary is the recommended way of installing Alertmanager. ### Docker images Docker images are available on [Quay.io](https://quay.io/repository/prometheus/alertmanager). ### Compiling the binary You can either `go get` it: ``` $ GO15VENDOREXPERIMENT=1 go get github.com/prometheus/alertmanager/cmd/... # cd $GOPATH/src/github.com/prometheus/alertmanager $ alertmanager --config.file= ``` Or checkout the source code and build manually: ``` $ mkdir -p $GOPATH/src/github.com/prometheus $ cd $GOPATH/src/github.com/prometheus $ git clone https://github.com/prometheus/alertmanager.git $ cd alertmanager $ make build $ ./alertmanager --config.file= ``` You can also build just one of the binaries in this repo by passing a name to the build function: ``` $ make build BINARIES=amtool ``` ## Example This is an example configuration that should cover most relevant aspects of the new YAML configuration format. The full documentation of the configuration can be found [here](https://prometheus.io/docs/alerting/configuration/). ```yaml global: # The smarthost and SMTP sender used for mail notifications. smtp_smarthost: 'localhost:25' smtp_from: 'alertmanager@example.org' # The root route on which each incoming alert enters. route: # The root route must not have any matchers as it is the entry point for # all alerts. It needs to have a receiver configured so alerts that do not # match any of the sub-routes are sent to someone. receiver: 'team-X-mails' # The labels by which incoming alerts are grouped together. For example, # multiple alerts coming in for cluster=A and alertname=LatencyHigh would # be batched into a single group. group_by: ['alertname', 'cluster'] # When a new group of alerts is created by an incoming alert, wait at # least 'group_wait' to send the initial notification. # This way ensures that you get multiple alerts for the same group that start # firing shortly after another are batched together on the first # notification. group_wait: 30s # When the first notification was sent, wait 'group_interval' to send a batch # of new alerts that started firing for that group. group_interval: 5m # If an alert has successfully been sent, wait 'repeat_interval' to # resend them. repeat_interval: 3h # All the above attributes are inherited by all child routes and can # overwritten on each. # The child route trees. routes: # This routes performs a regular expression match on alert labels to # catch alerts that are related to a list of services. - match_re: service: ^(foo1|foo2|baz)$ receiver: team-X-mails # The service has a sub-route for critical alerts, any alerts # that do not match, i.e. severity != critical, fall-back to the # parent node and are sent to 'team-X-mails' routes: - match: severity: critical receiver: team-X-pager - match: service: files receiver: team-Y-mails routes: - match: severity: critical receiver: team-Y-pager # This route handles all alerts coming from a database service. If there's # no team to handle it, it defaults to the DB team. - match: service: database receiver: team-DB-pager # Also group alerts by affected database. group_by: [alertname, cluster, database] routes: - match: owner: team-X receiver: team-X-pager - match: owner: team-Y receiver: team-Y-pager # Inhibition rules allow to mute a set of alerts given that another alert is # firing. # We use this to mute any warning-level notifications if the same alert is # already critical. inhibit_rules: - source_match: severity: 'critical' target_match: severity: 'warning' # Apply inhibition if the alertname is the same. equal: ['alertname'] receivers: - name: 'team-X-mails' email_configs: - to: 'team-X+alerts@example.org, team-Y+alerts@example.org' - name: 'team-X-pager' email_configs: - to: 'team-X+alerts-critical@example.org' pagerduty_configs: - routing_key: - name: 'team-Y-mails' email_configs: - to: 'team-Y+alerts@example.org' - name: 'team-Y-pager' pagerduty_configs: - routing_key: - name: 'team-DB-pager' pagerduty_configs: - routing_key: ``` ## Amtool `amtool` is a cli tool for interacting with the alertmanager api. It is bundled with all releases of alertmanager. ### Install Alternatively you can install with: ``` go get github.com/prometheus/alertmanager/cmd/amtool ``` ### Examples View all currently firing alerts ``` $ amtool alert Alertname Starts At Summary Test_Alert 2017-08-02 18:30:18 UTC This is a testing alert! Test_Alert 2017-08-02 18:30:18 UTC This is a testing alert! Check_Foo_Fails 2017-08-02 18:30:18 UTC This is a testing alert! Check_Foo_Fails 2017-08-02 18:30:18 UTC This is a testing alert! ``` View all currently firing alerts with extended output ``` $ amtool -o extended alert Labels Annotations Starts At Ends At Generator URL alertname="Test_Alert" instance="node0" link="https://example.com" summary="This is a testing alert!" 2017-08-02 18:31:24 UTC 0001-01-01 00:00:00 UTC http://my.testing.script.local alertname="Test_Alert" instance="node1" link="https://example.com" summary="This is a testing alert!" 2017-08-02 18:31:24 UTC 0001-01-01 00:00:00 UTC http://my.testing.script.local alertname="Check_Foo_Fails" instance="node0" link="https://example.com" summary="This is a testing alert!" 2017-08-02 18:31:24 UTC 0001-01-01 00:00:00 UTC http://my.testing.script.local alertname="Check_Foo_Fails" instance="node1" link="https://example.com" summary="This is a testing alert!" 2017-08-02 18:31:24 UTC 0001-01-01 00:00:00 UTC http://my.testing.script.local ``` In addition to viewing alerts you can use the rich query syntax provided by alertmanager ``` $ amtool -o extended alert query alertname="Test_Alert" Labels Annotations Starts At Ends At Generator URL alertname="Test_Alert" instance="node0" link="https://example.com" summary="This is a testing alert!" 2017-08-02 18:31:24 UTC 0001-01-01 00:00:00 UTC http://my.testing.script.local alertname="Test_Alert" instance="node1" link="https://example.com" summary="This is a testing alert!" 2017-08-02 18:31:24 UTC 0001-01-01 00:00:00 UTC http://my.testing.script.local $ amtool -o extended alert query instance=~".+1" Labels Annotations Starts At Ends At Generator URL alertname="Test_Alert" instance="node1" link="https://example.com" summary="This is a testing alert!" 2017-08-02 18:31:24 UTC 0001-01-01 00:00:00 UTC http://my.testing.script.local alertname="Check_Foo_Fails" instance="node1" link="https://example.com" summary="This is a testing alert!" 2017-08-02 18:31:24 UTC 0001-01-01 00:00:00 UTC http://my.testing.script.local $ amtool -o extended alert query alertname=~"Test.*" instance=~".+1" Labels Annotations Starts At Ends At Generator URL alertname="Test_Alert" instance="node1" link="https://example.com" summary="This is a testing alert!" 2017-08-02 18:31:24 UTC 0001-01-01 00:00:00 UTC http://my.testing.script.local ``` Silence an alert ``` $ amtool silence add alertname=Test_Alert b3ede22e-ca14-4aa0-932c-ca2f3445f926 $ amtool silence add alertname="Test_Alert" instance=~".+0" e48cb58a-0b17-49ba-b734-3585139b1d25 ``` View silences ``` $ amtool silence query ID Matchers Ends At Created By Comment b3ede22e-ca14-4aa0-932c-ca2f3445f926 alertname=Test_Alert 2017-08-02 19:54:50 UTC kellel $ amtool silence query instance=~".+0" ID Matchers Ends At Created By Comment e48cb58a-0b17-49ba-b734-3585139b1d25 alertname=Test_Alert instance=~.+0 2017-08-02 22:41:39 UTC kellel ``` Expire a silence ``` $ amtool silence expire b3ede22e-ca14-4aa0-932c-ca2f3445f926 ``` Expire all silences matching a query ``` $ amtool silence query instance=~".+0" ID Matchers Ends At Created By Comment e48cb58a-0b17-49ba-b734-3585139b1d25 alertname=Test_Alert instance=~.+0 2017-08-02 22:41:39 UTC kellel $ amtool silence expire $(amtool silence -q query instance=~".+0") $ amtool silence query instance=~".+0" ``` Expire all silences ``` $ amtool silence expire $(amtool silence query -q) ``` ### Config Amtool allows a config file to specify some options for convenience. The default config file paths are `$HOME/.config/amtool/config.yml` or `/etc/amtool/config.yml` An example configfile might look like the following: ``` # Define the path that amtool can find your `alertmanager` instance at alertmanager.url: "http://localhost:9093" # Override the default author. (unset defaults to your username) author: me@example.com # Force amtool to give you an error if you don't include a comment on a silence comment_required: true # Set a default output format. (unset defaults to simple) output: extended # Set a default receiver receiver: team-X-pager ``` ## High Availability > Warning: High Availability is under active development To create a highly available cluster of the Alertmanager the instances need to be configured to communicate with each other. This is configured using the `--cluster.*` flags. - `--cluster.listen-address` string: cluster listen address (default "0.0.0.0:9094") - `--cluster.advertise-address` string: cluster advertise address - `--cluster.peer` value: initial peers (repeat flag for each additional peer) - `--cluster.peer-timeout` value: peer timeout period (default "15s") - `--cluster.gossip-interval` value: cluster message propagation speed (default "200ms") - `--cluster.pushpull-interval` value: lower values will increase convergence speeds at expense of bandwidth (default "1m0s") - `--cluster.settle-timeout` value: maximum time to wait for cluster connections to settle before evaluating notifications. - `--cluster.tcp-timeout` value: timeout value for tcp connections, reads and writes (default "10s") - `--cluster.probe-timeout` value: time to wait for ack before marking node unhealthy (default "500ms") - `--cluster.probe-interval` value: interval between random node probes (default "1s") The chosen port in the `cluster.listen-address` flag is the port that needs to be specified in the `cluster.peer` flag of the other peers. To start a cluster of three peers on your local machine use `goreman` and the Procfile within this repository. goreman start To point your Prometheus 1.4, or later, instance to multiple Alertmanagers, configure them in your `prometheus.yml` configuration file, for example: ```yaml alerting: alertmanagers: - static_configs: - targets: - alertmanager1:9093 - alertmanager2:9093 - alertmanager3:9093 ``` > Important: Do not load balance traffic between Prometheus and its Alertmanagers, but instead point Prometheus to a list of all Alertmanagers. The Alertmanager implementation expects all alerts to be sent to all Alertmanagers to ensure high availability. ## Contributing to the Front-End Refer to [ui/app/CONTRIBUTING.md](ui/app/CONTRIBUTING.md). ## Architecture ![](doc/arch.svg) [travis]: https://travis-ci.org/prometheus/alertmanager [hub]: https://hub.docker.com/r/prom/alertmanager/ [circleci]: https://circleci.com/gh/prometheus/alertmanager [quay]: https://quay.io/repository/prometheus/alertmanager prometheus-alertmanager-0.15.3+ds/VERSION000066400000000000000000000000071341674552200201130ustar00rootroot000000000000000.15.3 prometheus-alertmanager-0.15.3+ds/api/000077500000000000000000000000001341674552200176175ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/api/api.go000066400000000000000000000451331341674552200207250ustar00rootroot00000000000000// Copyright 2015 Prometheus Team // 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" "errors" "fmt" "net/http" "regexp" "sort" "sync" "time" "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/common/model" "github.com/prometheus/common/route" "github.com/prometheus/common/version" "github.com/prometheus/prometheus/pkg/labels" "github.com/prometheus/alertmanager/cluster" "github.com/prometheus/alertmanager/config" "github.com/prometheus/alertmanager/dispatch" "github.com/prometheus/alertmanager/pkg/parse" "github.com/prometheus/alertmanager/provider" "github.com/prometheus/alertmanager/silence" "github.com/prometheus/alertmanager/silence/silencepb" "github.com/prometheus/alertmanager/types" ) var ( numReceivedAlerts = prometheus.NewCounterVec(prometheus.CounterOpts{ Namespace: "alertmanager", Name: "alerts_received_total", Help: "The total number of received alerts.", }, []string{"status"}) numInvalidAlerts = prometheus.NewCounter(prometheus.CounterOpts{ Namespace: "alertmanager", Name: "alerts_invalid_total", Help: "The total number of received alerts that were invalid.", }) ) func init() { numReceivedAlerts.WithLabelValues("firing") numReceivedAlerts.WithLabelValues("resolved") prometheus.MustRegister(numReceivedAlerts) prometheus.MustRegister(numInvalidAlerts) } var corsHeaders = map[string]string{ "Access-Control-Allow-Headers": "Accept, Authorization, Content-Type, Origin", "Access-Control-Allow-Methods": "GET, DELETE, OPTIONS", "Access-Control-Allow-Origin": "*", "Access-Control-Expose-Headers": "Date", "Cache-Control": "no-cache, no-store, must-revalidate", } // Enables cross-site script calls. func setCORS(w http.ResponseWriter) { for h, v := range corsHeaders { w.Header().Set(h, v) } } // API provides registration of handlers for API routes. type API struct { alerts provider.Alerts silences *silence.Silences config *config.Config route *dispatch.Route resolveTimeout time.Duration uptime time.Time peer *cluster.Peer logger log.Logger groups groupsFn getAlertStatus getAlertStatusFn mtx sync.RWMutex } type groupsFn func([]*labels.Matcher) dispatch.AlertOverview type getAlertStatusFn func(model.Fingerprint) types.AlertStatus // New returns a new API. func New( alerts provider.Alerts, silences *silence.Silences, gf groupsFn, sf getAlertStatusFn, peer *cluster.Peer, l log.Logger, ) *API { if l == nil { l = log.NewNopLogger() } return &API{ alerts: alerts, silences: silences, groups: gf, getAlertStatus: sf, uptime: time.Now(), peer: peer, logger: l, } } // Register registers the API handlers under their correct routes // in the given router. func (api *API) Register(r *route.Router) { wrap := func(f http.HandlerFunc) http.HandlerFunc { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { setCORS(w) f(w, r) }) } r.Options("/*path", wrap(func(w http.ResponseWriter, r *http.Request) {})) r.Get("/status", wrap(api.status)) r.Get("/receivers", wrap(api.receivers)) r.Get("/alerts/groups", wrap(api.alertGroups)) r.Get("/alerts", wrap(api.listAlerts)) r.Post("/alerts", wrap(api.addAlerts)) r.Get("/silences", wrap(api.listSilences)) r.Post("/silences", wrap(api.setSilence)) r.Get("/silence/:sid", wrap(api.getSilence)) r.Del("/silence/:sid", wrap(api.delSilence)) } // Update sets the configuration string to a new value. func (api *API) Update(cfg *config.Config, resolveTimeout time.Duration) error { api.mtx.Lock() defer api.mtx.Unlock() api.resolveTimeout = resolveTimeout api.config = cfg api.route = dispatch.NewRoute(cfg.Route, nil) return nil } type errorType string const ( errorNone errorType = "" errorInternal errorType = "server_error" errorBadData errorType = "bad_data" ) type apiError struct { typ errorType err error } func (e *apiError) Error() string { return fmt.Sprintf("%s: %s", e.typ, e.err) } func (api *API) receivers(w http.ResponseWriter, req *http.Request) { api.mtx.RLock() defer api.mtx.RUnlock() receivers := make([]string, 0, len(api.config.Receivers)) for _, r := range api.config.Receivers { receivers = append(receivers, r.Name) } api.respond(w, receivers) } func (api *API) status(w http.ResponseWriter, req *http.Request) { api.mtx.RLock() var status = struct { ConfigYAML string `json:"configYAML"` ConfigJSON *config.Config `json:"configJSON"` VersionInfo map[string]string `json:"versionInfo"` Uptime time.Time `json:"uptime"` ClusterStatus *clusterStatus `json:"clusterStatus"` }{ ConfigYAML: api.config.String(), ConfigJSON: api.config, VersionInfo: map[string]string{ "version": version.Version, "revision": version.Revision, "branch": version.Branch, "buildUser": version.BuildUser, "buildDate": version.BuildDate, "goVersion": version.GoVersion, }, Uptime: api.uptime, ClusterStatus: getClusterStatus(api.peer), } api.mtx.RUnlock() api.respond(w, status) } type peerStatus struct { Name string `json:"name"` Address string `json:"address"` } type clusterStatus struct { Name string `json:"name"` Status string `json:"status"` Peers []peerStatus `json:"peers"` } func getClusterStatus(p *cluster.Peer) *clusterStatus { if p == nil { return nil } s := &clusterStatus{Name: p.Name(), Status: p.Status()} for _, n := range p.Peers() { s.Peers = append(s.Peers, peerStatus{ Name: n.Name, Address: n.Address(), }) } return s } func (api *API) alertGroups(w http.ResponseWriter, r *http.Request) { var err error matchers := []*labels.Matcher{} if filter := r.FormValue("filter"); filter != "" { matchers, err = parse.Matchers(filter) if err != nil { api.respondError(w, apiError{ typ: errorBadData, err: err, }, nil) return } } groups := api.groups(matchers) api.respond(w, groups) } func (api *API) listAlerts(w http.ResponseWriter, r *http.Request) { var ( err error receiverFilter *regexp.Regexp // Initialize result slice to prevent api returning `null` when there // are no alerts present res = []*dispatch.APIAlert{} matchers = []*labels.Matcher{} showActive, showInhibited bool showSilenced, showUnprocessed bool ) getBoolParam := func(name string) (bool, error) { v := r.FormValue(name) if v == "" { return true, nil } if v == "false" { return false, nil } if v != "true" { err := fmt.Errorf("parameter %q can either be 'true' or 'false', not %q", name, v) api.respondError(w, apiError{ typ: errorBadData, err: err, }, nil) return false, err } return true, nil } if filter := r.FormValue("filter"); filter != "" { matchers, err = parse.Matchers(filter) if err != nil { api.respondError(w, apiError{ typ: errorBadData, err: err, }, nil) return } } showActive, err = getBoolParam("active") if err != nil { return } showSilenced, err = getBoolParam("silenced") if err != nil { return } showInhibited, err = getBoolParam("inhibited") if err != nil { return } showUnprocessed, err = getBoolParam("unprocessed") if err != nil { return } if receiverParam := r.FormValue("receiver"); receiverParam != "" { receiverFilter, err = regexp.Compile("^(?:" + receiverParam + ")$") if err != nil { api.respondError(w, apiError{ typ: errorBadData, err: fmt.Errorf( "failed to parse receiver param: %s", receiverParam, ), }, nil) return } } alerts := api.alerts.GetPending() defer alerts.Close() api.mtx.RLock() // TODO(fabxc): enforce a sensible timeout. for a := range alerts.Next() { if err = alerts.Err(); err != nil { break } routes := api.route.Match(a.Labels) receivers := make([]string, 0, len(routes)) for _, r := range routes { receivers = append(receivers, r.RouteOpts.Receiver) } if receiverFilter != nil && !receiversMatchFilter(receivers, receiverFilter) { continue } if !alertMatchesFilterLabels(&a.Alert, matchers) { continue } // Continue if the alert is resolved. if !a.Alert.EndsAt.IsZero() && a.Alert.EndsAt.Before(time.Now()) { continue } status := api.getAlertStatus(a.Fingerprint()) if !showActive && status.State == types.AlertStateActive { continue } if !showUnprocessed && status.State == types.AlertStateUnprocessed { continue } if !showSilenced && len(status.SilencedBy) != 0 { continue } if !showInhibited && len(status.InhibitedBy) != 0 { continue } apiAlert := &dispatch.APIAlert{ Alert: &a.Alert, Status: status, Receivers: receivers, Fingerprint: a.Fingerprint().String(), } res = append(res, apiAlert) } api.mtx.RUnlock() if err != nil { api.respondError(w, apiError{ typ: errorInternal, err: err, }, nil) return } sort.Slice(res, func(i, j int) bool { return res[i].Fingerprint < res[j].Fingerprint }) api.respond(w, res) } func receiversMatchFilter(receivers []string, filter *regexp.Regexp) bool { for _, r := range receivers { if filter.MatchString(r) { return true } } return false } func alertMatchesFilterLabels(a *model.Alert, matchers []*labels.Matcher) bool { sms := make(map[string]string) for name, value := range a.Labels { sms[string(name)] = string(value) } return matchFilterLabels(matchers, sms) } func (api *API) addAlerts(w http.ResponseWriter, r *http.Request) { var alerts []*types.Alert if err := api.receive(r, &alerts); err != nil { api.respondError(w, apiError{ typ: errorBadData, err: err, }, nil) return } api.insertAlerts(w, r, alerts...) } func (api *API) insertAlerts(w http.ResponseWriter, r *http.Request, alerts ...*types.Alert) { now := time.Now() api.mtx.RLock() resolveTimeout := api.resolveTimeout api.mtx.RUnlock() for _, alert := range alerts { alert.UpdatedAt = now // Ensure StartsAt is set. if alert.StartsAt.IsZero() { if alert.EndsAt.IsZero() { alert.StartsAt = now } else { alert.StartsAt = alert.EndsAt } } // If no end time is defined, set a timeout after which an alert // is marked resolved if it is not updated. if alert.EndsAt.IsZero() { alert.Timeout = true alert.EndsAt = now.Add(resolveTimeout) } if alert.EndsAt.After(time.Now()) { numReceivedAlerts.WithLabelValues("firing").Inc() } else { numReceivedAlerts.WithLabelValues("resolved").Inc() } } // Make a best effort to insert all alerts that are valid. var ( validAlerts = make([]*types.Alert, 0, len(alerts)) validationErrs = &types.MultiError{} ) for _, a := range alerts { removeEmptyLabels(a.Labels) if err := a.Validate(); err != nil { validationErrs.Add(err) numInvalidAlerts.Inc() continue } validAlerts = append(validAlerts, a) } if err := api.alerts.Put(validAlerts...); err != nil { api.respondError(w, apiError{ typ: errorInternal, err: err, }, nil) return } if validationErrs.Len() > 0 { api.respondError(w, apiError{ typ: errorBadData, err: validationErrs, }, nil) return } api.respond(w, nil) } func removeEmptyLabels(ls model.LabelSet) { for k, v := range ls { if string(v) == "" { delete(ls, k) } } } func (api *API) setSilence(w http.ResponseWriter, r *http.Request) { var sil types.Silence if err := api.receive(r, &sil); err != nil { api.respondError(w, apiError{ typ: errorBadData, err: err, }, nil) return } // This is an API only validation, it cannot be done internally // because the expired silence is semantically important. // But one should not be able to create expired silences, that // won't have any use. if sil.Expired() { api.respondError(w, apiError{ typ: errorBadData, err: errors.New("start time must not be equal to end time"), }, nil) return } if sil.EndsAt.Before(time.Now()) { api.respondError(w, apiError{ typ: errorBadData, err: errors.New("end time can't be in the past"), }, nil) return } psil, err := silenceToProto(&sil) if err != nil { api.respondError(w, apiError{ typ: errorBadData, err: err, }, nil) return } sid, err := api.silences.Set(psil) if err != nil { api.respondError(w, apiError{ typ: errorBadData, err: err, }, nil) return } api.respond(w, struct { SilenceID string `json:"silenceId"` }{ SilenceID: sid, }) } func (api *API) getSilence(w http.ResponseWriter, r *http.Request) { sid := route.Param(r.Context(), "sid") sils, err := api.silences.Query(silence.QIDs(sid)) if err != nil || len(sils) == 0 { http.Error(w, fmt.Sprint("Error getting silence: ", err), http.StatusNotFound) return } sil, err := silenceFromProto(sils[0]) if err != nil { api.respondError(w, apiError{ typ: errorInternal, err: err, }, nil) return } api.respond(w, sil) } func (api *API) delSilence(w http.ResponseWriter, r *http.Request) { sid := route.Param(r.Context(), "sid") if err := api.silences.Expire(sid); err != nil { api.respondError(w, apiError{ typ: errorBadData, err: err, }, nil) return } api.respond(w, nil) } func (api *API) listSilences(w http.ResponseWriter, r *http.Request) { psils, err := api.silences.Query() if err != nil { api.respondError(w, apiError{ typ: errorInternal, err: err, }, nil) return } matchers := []*labels.Matcher{} if filter := r.FormValue("filter"); filter != "" { matchers, err = parse.Matchers(filter) if err != nil { api.respondError(w, apiError{ typ: errorBadData, err: err, }, nil) return } } sils := []*types.Silence{} for _, ps := range psils { s, err := silenceFromProto(ps) if err != nil { api.respondError(w, apiError{ typ: errorInternal, err: err, }, nil) return } if !silenceMatchesFilterLabels(s, matchers) { continue } sils = append(sils, s) } var active, pending, expired []*types.Silence for _, s := range sils { switch s.Status.State { case types.SilenceStateActive: active = append(active, s) case types.SilenceStatePending: pending = append(pending, s) case types.SilenceStateExpired: expired = append(expired, s) } } sort.Slice(active, func(i int, j int) bool { return active[i].EndsAt.Before(active[j].EndsAt) }) sort.Slice(pending, func(i int, j int) bool { return pending[i].StartsAt.Before(pending[j].EndsAt) }) sort.Slice(expired, func(i int, j int) bool { return expired[i].EndsAt.After(expired[j].EndsAt) }) // Initialize silences explicitly to an empty list (instead of nil) // So that it does not get converted to "null" in JSON. silences := []*types.Silence{} silences = append(silences, active...) silences = append(silences, pending...) silences = append(silences, expired...) api.respond(w, silences) } func silenceMatchesFilterLabels(s *types.Silence, matchers []*labels.Matcher) bool { sms := make(map[string]string) for _, m := range s.Matchers { sms[m.Name] = m.Value } return matchFilterLabels(matchers, sms) } func matchFilterLabels(matchers []*labels.Matcher, sms map[string]string) bool { for _, m := range matchers { v, prs := sms[m.Name] switch m.Type { case labels.MatchNotRegexp, labels.MatchNotEqual: if string(m.Value) == "" && prs { continue } if !m.Matches(string(v)) { return false } default: if string(m.Value) == "" && !prs { continue } if !prs || !m.Matches(string(v)) { return false } } } return true } func silenceToProto(s *types.Silence) (*silencepb.Silence, error) { sil := &silencepb.Silence{ Id: s.ID, StartsAt: s.StartsAt, EndsAt: s.EndsAt, UpdatedAt: s.UpdatedAt, Comment: s.Comment, CreatedBy: s.CreatedBy, } for _, m := range s.Matchers { matcher := &silencepb.Matcher{ Name: m.Name, Pattern: m.Value, Type: silencepb.Matcher_EQUAL, } if m.IsRegex { matcher.Type = silencepb.Matcher_REGEXP } sil.Matchers = append(sil.Matchers, matcher) } return sil, nil } func silenceFromProto(s *silencepb.Silence) (*types.Silence, error) { sil := &types.Silence{ ID: s.Id, StartsAt: s.StartsAt, EndsAt: s.EndsAt, UpdatedAt: s.UpdatedAt, Status: types.SilenceStatus{ State: types.CalcSilenceState(s.StartsAt, s.EndsAt), }, Comment: s.Comment, CreatedBy: s.CreatedBy, } for _, m := range s.Matchers { matcher := &types.Matcher{ Name: m.Name, Value: m.Pattern, } switch m.Type { case silencepb.Matcher_EQUAL: case silencepb.Matcher_REGEXP: matcher.IsRegex = true default: return nil, fmt.Errorf("unknown matcher type") } sil.Matchers = append(sil.Matchers, matcher) } return sil, nil } type status string const ( statusSuccess status = "success" statusError status = "error" ) type response struct { Status status `json:"status"` Data interface{} `json:"data,omitempty"` ErrorType errorType `json:"errorType,omitempty"` Error string `json:"error,omitempty"` } func (api *API) respond(w http.ResponseWriter, data interface{}) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(200) b, err := json.Marshal(&response{ Status: statusSuccess, Data: data, }) if err != nil { level.Error(api.logger).Log("msg", "Error marshalling JSON", "err", err) return } w.Write(b) } func (api *API) respondError(w http.ResponseWriter, apiErr apiError, data interface{}) { w.Header().Set("Content-Type", "application/json") switch apiErr.typ { case errorBadData: w.WriteHeader(http.StatusBadRequest) case errorInternal: w.WriteHeader(http.StatusInternalServerError) default: panic(fmt.Sprintf("unknown error type %q", apiErr.Error())) } b, err := json.Marshal(&response{ Status: statusError, ErrorType: apiErr.typ, Error: apiErr.err.Error(), Data: data, }) if err != nil { return } level.Error(api.logger).Log("msg", "API error", "err", apiErr.Error()) w.Write(b) } func (api *API) receive(r *http.Request, v interface{}) error { dec := json.NewDecoder(r.Body) defer r.Body.Close() err := dec.Decode(v) if err != nil { level.Debug(api.logger).Log("msg", "Decoding request failed", "err", err) } return err } prometheus-alertmanager-0.15.3+ds/api/api_test.go000066400000000000000000000361661341674552200217720ustar00rootroot00000000000000// Copyright 2018 Prometheus Team // 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 ( "bytes" "encoding/json" "errors" "fmt" "io/ioutil" "net/http" "net/http/httptest" "regexp" "testing" "time" "github.com/prometheus/alertmanager/config" "github.com/prometheus/alertmanager/dispatch" "github.com/prometheus/alertmanager/provider" "github.com/prometheus/alertmanager/types" "github.com/prometheus/common/model" "github.com/prometheus/prometheus/pkg/labels" "github.com/stretchr/testify/require" ) // fakeAlerts is a struct implementing the provider.Alerts interface for tests. type fakeAlerts struct { fps map[model.Fingerprint]int alerts []*types.Alert err error } func newFakeAlerts(alerts []*types.Alert, withErr bool) *fakeAlerts { fps := make(map[model.Fingerprint]int) for i, a := range alerts { fps[a.Fingerprint()] = i } f := &fakeAlerts{ alerts: alerts, fps: fps, } if withErr { f.err = errors.New("Error occured") } return f } func (f *fakeAlerts) Subscribe() provider.AlertIterator { return nil } func (f *fakeAlerts) Get(model.Fingerprint) (*types.Alert, error) { return nil, nil } func (f *fakeAlerts) Put(alerts ...*types.Alert) error { return f.err } func (f *fakeAlerts) GetPending() provider.AlertIterator { ch := make(chan *types.Alert) done := make(chan struct{}) go func() { defer close(ch) for _, a := range f.alerts { ch <- a } }() return provider.NewAlertIterator(ch, done, f.err) } func groupAlerts([]*labels.Matcher) dispatch.AlertOverview { return dispatch.AlertOverview{} } func newGetAlertStatus(f *fakeAlerts) func(model.Fingerprint) types.AlertStatus { return func(fp model.Fingerprint) types.AlertStatus { status := types.AlertStatus{SilencedBy: []string{}, InhibitedBy: []string{}} i, ok := f.fps[fp] if !ok { return status } alert := f.alerts[i] switch alert.Labels["state"] { case "active": status.State = types.AlertStateActive case "unprocessed": status.State = types.AlertStateUnprocessed case "suppressed": status.State = types.AlertStateSuppressed } if alert.Labels["silenced_by"] != "" { status.SilencedBy = append(status.SilencedBy, string(alert.Labels["silenced_by"])) } if alert.Labels["inhibited_by"] != "" { status.InhibitedBy = append(status.InhibitedBy, string(alert.Labels["inhibited_by"])) } return status } } func TestAddAlerts(t *testing.T) { now := func(offset int) time.Time { return time.Now().Add(time.Duration(offset) * time.Second) } for i, tc := range []struct { start, end time.Time err bool code int }{ {time.Time{}, time.Time{}, false, 200}, {now(0), time.Time{}, false, 200}, {time.Time{}, now(-1), false, 200}, {time.Time{}, now(0), false, 200}, {time.Time{}, now(1), false, 200}, {now(-2), now(-1), false, 200}, {now(1), now(2), false, 200}, {now(1), now(0), false, 400}, {now(0), time.Time{}, true, 500}, } { alerts := []model.Alert{{ StartsAt: tc.start, EndsAt: tc.end, Labels: model.LabelSet{"label1": "test1"}, Annotations: model.LabelSet{"annotation1": "some text"}, }} b, err := json.Marshal(&alerts) if err != nil { t.Errorf("Unexpected error %v", err) } alertsProvider := newFakeAlerts([]*types.Alert{}, tc.err) api := New(alertsProvider, nil, groupAlerts, newGetAlertStatus(alertsProvider), nil, nil) r, err := http.NewRequest("POST", "/api/v1/alerts", bytes.NewReader(b)) w := httptest.NewRecorder() if err != nil { t.Errorf("Unexpected error %v", err) } api.addAlerts(w, r) res := w.Result() body, _ := ioutil.ReadAll(res.Body) require.Equal(t, tc.code, w.Code, fmt.Sprintf("test case: %d, StartsAt %v, EndsAt %v, Response: %s", i, tc.start, tc.end, string(body))) } } func TestListAlerts(t *testing.T) { now := time.Now() alerts := []*types.Alert{ &types.Alert{ Alert: model.Alert{ Labels: model.LabelSet{"state": "active", "alertname": "alert1"}, StartsAt: now.Add(-time.Minute), }, }, &types.Alert{ Alert: model.Alert{ Labels: model.LabelSet{"state": "unprocessed", "alertname": "alert2"}, StartsAt: now.Add(-time.Minute), }, }, &types.Alert{ Alert: model.Alert{ Labels: model.LabelSet{"state": "suppressed", "silenced_by": "abc", "alertname": "alert3"}, StartsAt: now.Add(-time.Minute), }, }, &types.Alert{ Alert: model.Alert{ Labels: model.LabelSet{"state": "suppressed", "inhibited_by": "abc", "alertname": "alert4"}, StartsAt: now.Add(-time.Minute), }, }, &types.Alert{ Alert: model.Alert{ Labels: model.LabelSet{"alertname": "alert5"}, StartsAt: now.Add(-2 * time.Minute), EndsAt: now.Add(-time.Minute), }, }, } for i, tc := range []struct { err bool params map[string]string code int anames []string }{ { false, map[string]string{}, 200, []string{"alert1", "alert2", "alert3", "alert4"}, }, { false, map[string]string{"active": "true", "unprocessed": "true", "silenced": "true", "inhibited": "true"}, 200, []string{"alert1", "alert2", "alert3", "alert4"}, }, { false, map[string]string{"active": "false", "unprocessed": "true", "silenced": "true", "inhibited": "true"}, 200, []string{"alert2", "alert3", "alert4"}, }, { false, map[string]string{"active": "true", "unprocessed": "false", "silenced": "true", "inhibited": "true"}, 200, []string{"alert1", "alert3", "alert4"}, }, { false, map[string]string{"active": "true", "unprocessed": "true", "silenced": "false", "inhibited": "true"}, 200, []string{"alert1", "alert2", "alert4"}, }, { false, map[string]string{"active": "true", "unprocessed": "true", "silenced": "true", "inhibited": "false"}, 200, []string{"alert1", "alert2", "alert3"}, }, { false, map[string]string{"filter": "{alertname=\"alert3\""}, 200, []string{"alert3"}, }, { false, map[string]string{"filter": "{alertname"}, 400, []string{}, }, { false, map[string]string{"receiver": "other"}, 200, []string{}, }, { false, map[string]string{"active": "invalid"}, 400, []string{}, }, { true, map[string]string{}, 500, []string{}, }, } { alertsProvider := newFakeAlerts(alerts, tc.err) api := New(alertsProvider, nil, groupAlerts, newGetAlertStatus(alertsProvider), nil, nil) api.route = dispatch.NewRoute(&config.Route{Receiver: "def-receiver"}, nil) r, err := http.NewRequest("GET", "/api/v1/alerts", nil) if err != nil { t.Fatalf("Unexpected error %v", err) } q := r.URL.Query() for k, v := range tc.params { q.Add(k, v) } r.URL.RawQuery = q.Encode() w := httptest.NewRecorder() api.listAlerts(w, r) body, _ := ioutil.ReadAll(w.Result().Body) var res response err = json.Unmarshal(body, &res) if err != nil { t.Fatalf("Unexpected error %v", err) } require.Equal(t, tc.code, w.Code, fmt.Sprintf("test case: %d, response: %s", i, string(body))) if w.Code != 200 { continue } // Data needs to be serialized/deserialized to be converted to the real type. b, err := json.Marshal(res.Data) if err != nil { t.Fatalf("Unexpected error %v", err) } retAlerts := []*dispatch.APIAlert{} err = json.Unmarshal(b, &retAlerts) if err != nil { t.Fatalf("Unexpected error %v", err) } anames := []string{} for _, a := range retAlerts { name, ok := a.Labels["alertname"] if ok { anames = append(anames, string(name)) } } require.Equal(t, tc.anames, anames, fmt.Sprintf("test case: %d, alert names are not equal", i)) } } func TestAlertFiltering(t *testing.T) { type test struct { alert *model.Alert msg string expected bool } // Equal equal, err := labels.NewMatcher(labels.MatchEqual, "label1", "test1") if err != nil { t.Errorf("Unexpected error %v", err) } tests := []test{ {&model.Alert{Labels: model.LabelSet{"label1": "test1"}}, "label1=test1", true}, {&model.Alert{Labels: model.LabelSet{"label1": "test2"}}, "label1=test2", false}, {&model.Alert{Labels: model.LabelSet{"label2": "test2"}}, "label2=test2", false}, } for _, test := range tests { actual := alertMatchesFilterLabels(test.alert, []*labels.Matcher{equal}) msg := fmt.Sprintf("Expected %t for %s", test.expected, test.msg) require.Equal(t, test.expected, actual, msg) } // Not Equal notEqual, err := labels.NewMatcher(labels.MatchNotEqual, "label1", "test1") if err != nil { t.Errorf("Unexpected error %v", err) } tests = []test{ {&model.Alert{Labels: model.LabelSet{"label1": "test1"}}, "label1!=test1", false}, {&model.Alert{Labels: model.LabelSet{"label1": "test2"}}, "label1!=test2", true}, {&model.Alert{Labels: model.LabelSet{"label2": "test2"}}, "label2!=test2", true}, } for _, test := range tests { actual := alertMatchesFilterLabels(test.alert, []*labels.Matcher{notEqual}) msg := fmt.Sprintf("Expected %t for %s", test.expected, test.msg) require.Equal(t, test.expected, actual, msg) } // Regexp Equal regexpEqual, err := labels.NewMatcher(labels.MatchRegexp, "label1", "tes.*") if err != nil { t.Errorf("Unexpected error %v", err) } tests = []test{ {&model.Alert{Labels: model.LabelSet{"label1": "test1"}}, "label1=~test1", true}, {&model.Alert{Labels: model.LabelSet{"label1": "test2"}}, "label1=~test2", true}, {&model.Alert{Labels: model.LabelSet{"label2": "test2"}}, "label2=~test2", false}, } for _, test := range tests { actual := alertMatchesFilterLabels(test.alert, []*labels.Matcher{regexpEqual}) msg := fmt.Sprintf("Expected %t for %s", test.expected, test.msg) require.Equal(t, test.expected, actual, msg) } // Regexp Not Equal regexpNotEqual, err := labels.NewMatcher(labels.MatchNotRegexp, "label1", "tes.*") if err != nil { t.Errorf("Unexpected error %v", err) } tests = []test{ {&model.Alert{Labels: model.LabelSet{"label1": "test1"}}, "label1!~test1", false}, {&model.Alert{Labels: model.LabelSet{"label1": "test2"}}, "label1!~test2", false}, {&model.Alert{Labels: model.LabelSet{"label2": "test2"}}, "label2!~test2", true}, } for _, test := range tests { actual := alertMatchesFilterLabels(test.alert, []*labels.Matcher{regexpNotEqual}) msg := fmt.Sprintf("Expected %t for %s", test.expected, test.msg) require.Equal(t, test.expected, actual, msg) } } func TestSilenceFiltering(t *testing.T) { type test struct { silence *types.Silence msg string expected bool } // Equal equal, err := labels.NewMatcher(labels.MatchEqual, "label1", "test1") if err != nil { t.Errorf("Unexpected error %v", err) } tests := []test{ { &types.Silence{Matchers: newMatcher(model.LabelSet{"label1": "test1"})}, "label1=test1", true, }, { &types.Silence{Matchers: newMatcher(model.LabelSet{"label1": "test2"})}, "label1=test2", false, }, { &types.Silence{Matchers: newMatcher(model.LabelSet{"label2": "test2"})}, "label2=test2", false, }, } for _, test := range tests { actual := silenceMatchesFilterLabels(test.silence, []*labels.Matcher{equal}) msg := fmt.Sprintf("Expected %t for %s", test.expected, test.msg) require.Equal(t, test.expected, actual, msg) } // Not Equal notEqual, err := labels.NewMatcher(labels.MatchNotEqual, "label1", "test1") if err != nil { t.Errorf("Unexpected error %v", err) } tests = []test{ { &types.Silence{Matchers: newMatcher(model.LabelSet{"label1": "test1"})}, "label1!=test1", false, }, { &types.Silence{Matchers: newMatcher(model.LabelSet{"label1": "test2"})}, "label1!=test2", true, }, { &types.Silence{Matchers: newMatcher(model.LabelSet{"label2": "test2"})}, "label2!=test2", true, }, } for _, test := range tests { actual := silenceMatchesFilterLabels(test.silence, []*labels.Matcher{notEqual}) msg := fmt.Sprintf("Expected %t for %s", test.expected, test.msg) require.Equal(t, test.expected, actual, msg) } // Regexp Equal regexpEqual, err := labels.NewMatcher(labels.MatchRegexp, "label1", "tes.*") if err != nil { t.Errorf("Unexpected error %v", err) } tests = []test{ { &types.Silence{Matchers: newMatcher(model.LabelSet{"label1": "test1"})}, "label1=~test1", true, }, { &types.Silence{Matchers: newMatcher(model.LabelSet{"label1": "test2"})}, "label1=~test2", true, }, { &types.Silence{Matchers: newMatcher(model.LabelSet{"label2": "test2"})}, "label2=~test2", false, }, } for _, test := range tests { actual := silenceMatchesFilterLabels(test.silence, []*labels.Matcher{regexpEqual}) msg := fmt.Sprintf("Expected %t for %s", test.expected, test.msg) require.Equal(t, test.expected, actual, msg) } // Regexp Not Equal regexpNotEqual, err := labels.NewMatcher(labels.MatchNotRegexp, "label1", "tes.*") if err != nil { t.Errorf("Unexpected error %v", err) } tests = []test{ { &types.Silence{Matchers: newMatcher(model.LabelSet{"label1": "test1"})}, "label1!~test1", false, }, { &types.Silence{Matchers: newMatcher(model.LabelSet{"label1": "test2"})}, "label1!~test2", false, }, { &types.Silence{Matchers: newMatcher(model.LabelSet{"label2": "test2"})}, "label2!~test2", true, }, } for _, test := range tests { actual := silenceMatchesFilterLabels(test.silence, []*labels.Matcher{regexpNotEqual}) msg := fmt.Sprintf("Expected %t for %s", test.expected, test.msg) require.Equal(t, test.expected, actual, msg) } } func TestReceiversMatchFilter(t *testing.T) { receivers := []string{"pagerduty", "slack", "hipchat"} filter, err := regexp.Compile(fmt.Sprintf("^(?:%s)$", "hip.*")) if err != nil { t.Errorf("Unexpected error %v", err) } require.True(t, receiversMatchFilter(receivers, filter)) filter, err = regexp.Compile(fmt.Sprintf("^(?:%s)$", "hip")) if err != nil { t.Errorf("Unexpected error %v", err) } require.False(t, receiversMatchFilter(receivers, filter)) } func TestMatchFilterLabels(t *testing.T) { testCases := []struct { matcher labels.MatchType expected bool }{ {labels.MatchEqual, true}, {labels.MatchRegexp, true}, {labels.MatchNotEqual, false}, {labels.MatchNotRegexp, false}, } for _, tc := range testCases { l, err := labels.NewMatcher(tc.matcher, "foo", "") require.NoError(t, err) sms := map[string]string{ "baz": "bar", } ls := []*labels.Matcher{l} require.Equal(t, tc.expected, matchFilterLabels(ls, sms)) l, err = labels.NewMatcher(tc.matcher, "foo", "") require.NoError(t, err) sms = map[string]string{ "baz": "bar", "foo": "quux", } ls = []*labels.Matcher{l} require.NotEqual(t, tc.expected, matchFilterLabels(ls, sms)) } } func newMatcher(labelSet model.LabelSet) types.Matchers { matchers := make([]*types.Matcher, 0, len(labelSet)) for key, val := range labelSet { matchers = append(matchers, types.NewMatcher(key, string(val))) } return matchers } prometheus-alertmanager-0.15.3+ds/cli/000077500000000000000000000000001341674552200176155ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/cli/alert.go000066400000000000000000000101411341674552200212500ustar00rootroot00000000000000// Copyright 2018 Prometheus Team // 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 cli import ( "context" "errors" "fmt" "strings" "github.com/prometheus/client_golang/api" "gopkg.in/alecthomas/kingpin.v2" "github.com/prometheus/alertmanager/cli/format" "github.com/prometheus/alertmanager/client" "github.com/prometheus/alertmanager/pkg/parse" ) type alertQueryCmd struct { inhibited, silenced, active, unprocessed bool receiver string matcherGroups []string } const alertHelp = `View and search through current alerts. Amtool has a simplified prometheus query syntax, but contains robust support for bash variable expansions. The non-option section of arguments constructs a list of "Matcher Groups" that will be used to filter your query. The following examples will attempt to show this behaviour in action: amtool alert query alertname=foo node=bar This query will match all alerts with the alertname=foo and node=bar label value pairs set. amtool alert query foo node=bar If alertname is omitted and the first argument does not contain a '=' or a '=~' then it will be assumed to be the value of the alertname pair. amtool alert query 'alertname=~foo.*' As well as direct equality, regex matching is also supported. The '=~' syntax (similar to prometheus) is used to represent a regex match. Regex matching can be used in combination with a direct match. Amtool supports several flags for filtering the returned alerts by state (inhibited, silenced, active, unprocessed). If none of these flags is given, only active alerts are returned. ` func configureAlertCmd(app *kingpin.Application) { var ( a = &alertQueryCmd{} alertCmd = app.Command("alert", alertHelp).PreAction(requireAlertManagerURL) queryCmd = alertCmd.Command("query", alertHelp).Default() ) queryCmd.Flag("inhibited", "Show inhibited alerts").Short('i').BoolVar(&a.inhibited) queryCmd.Flag("silenced", "Show silenced alerts").Short('s').BoolVar(&a.silenced) queryCmd.Flag("active", "Show active alerts").Short('a').BoolVar(&a.active) queryCmd.Flag("unprocessed", "Show unprocessed alerts").Short('u').BoolVar(&a.unprocessed) queryCmd.Flag("receiver", "Show alerts matching receiver (Supports regex syntax)").Short('r').StringVar(&a.receiver) queryCmd.Arg("matcher-groups", "Query filter").StringsVar(&a.matcherGroups) queryCmd.Action(a.queryAlerts) } func (a *alertQueryCmd) queryAlerts(ctx *kingpin.ParseContext) error { var filterString = "" if len(a.matcherGroups) == 1 { // If the parser fails then we likely don't have a (=|=~|!=|!~) so lets // assume that the user wants alertname= and prepend `alertname=` // to the front. _, err := parse.Matcher(a.matcherGroups[0]) if err != nil { filterString = fmt.Sprintf("{alertname=%s}", a.matcherGroups[0]) } else { filterString = fmt.Sprintf("{%s}", strings.Join(a.matcherGroups, ",")) } } else if len(a.matcherGroups) > 1 { filterString = fmt.Sprintf("{%s}", strings.Join(a.matcherGroups, ",")) } c, err := api.NewClient(api.Config{Address: alertmanagerURL.String()}) if err != nil { return err } alertAPI := client.NewAlertAPI(c) // If no selector was passed, default to showing active alerts. if !a.silenced && !a.inhibited && !a.active && !a.unprocessed { a.active = true } fetchedAlerts, err := alertAPI.List(context.Background(), filterString, a.receiver, a.silenced, a.inhibited, a.active, a.unprocessed) if err != nil { return err } formatter, found := format.Formatters[output] if !found { return errors.New("unknown output formatter") } return formatter.FormatAlerts(fetchedAlerts) } prometheus-alertmanager-0.15.3+ds/cli/check_config.go000066400000000000000000000051611341674552200225510ustar00rootroot00000000000000// Copyright 2018 Prometheus Team // 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 cli import ( "fmt" "os" "github.com/prometheus/alertmanager/config" "github.com/prometheus/alertmanager/template" "gopkg.in/alecthomas/kingpin.v2" ) // TODO: This can just be a type that is []string, doesn't have to be a struct type checkConfigCmd struct { files []string } const checkConfigHelp = `Validate alertmanager config files Will validate the syntax and schema for alertmanager config file and associated templates. Non existing templates will not trigger errors. ` func configureCheckConfigCmd(app *kingpin.Application) { var ( c = &checkConfigCmd{} checkCmd = app.Command("check-config", checkConfigHelp) ) checkCmd.Arg("check-files", "Files to be validated").ExistingFilesVar(&c.files) checkCmd.Action(c.checkConfig) } func (c *checkConfigCmd) checkConfig(ctx *kingpin.ParseContext) error { return CheckConfig(c.files) } func CheckConfig(args []string) error { if len(args) == 0 { stat, err := os.Stdin.Stat() if err != nil { kingpin.Fatalf("Failed to stat standard input: %v", err) } if (stat.Mode() & os.ModeCharDevice) != 0 { kingpin.Fatalf("Failed to read from standard input") } args = []string{os.Stdin.Name()} } failed := 0 for _, arg := range args { fmt.Printf("Checking '%s'", arg) cfg, _, err := config.LoadFile(arg) if err != nil { fmt.Printf(" FAILED: %s\n", err) failed++ } else { fmt.Printf(" SUCCESS\n") } if cfg != nil { fmt.Println("Found:") if cfg.Global != nil { fmt.Println(" - global config") } if cfg.Route != nil { fmt.Println(" - route") } fmt.Printf(" - %d inhibit rules\n", len(cfg.InhibitRules)) fmt.Printf(" - %d receivers\n", len(cfg.Receivers)) fmt.Printf(" - %d templates\n", len(cfg.Templates)) if len(cfg.Templates) > 0 { _, err = template.FromGlobs(cfg.Templates...) if err != nil { fmt.Printf(" FAILED: %s\n", err) failed++ } else { fmt.Printf(" SUCCESS\n") } } } fmt.Printf("\n") } if failed > 0 { return fmt.Errorf("failed to validate %d file(s)", failed) } return nil } prometheus-alertmanager-0.15.3+ds/cli/check_config_test.go000066400000000000000000000016271341674552200236130ustar00rootroot00000000000000// Copyright 2018 Prometheus Team // 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 cli import ( "testing" ) func TestCheckConfig(t *testing.T) { err := CheckConfig([]string{"testdata/conf.good.yml"}) if err != nil { t.Fatalf("checking valid config file failed with: %v", err) } err = CheckConfig([]string{"testdata/conf.bad.yml"}) if err == nil { t.Fatalf("failed to detect invalid file.") } } prometheus-alertmanager-0.15.3+ds/cli/config.go000066400000000000000000000032421341674552200214120ustar00rootroot00000000000000// Copyright 2018 Prometheus Team // 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 cli import ( "context" "errors" "github.com/prometheus/client_golang/api" "gopkg.in/alecthomas/kingpin.v2" "github.com/prometheus/alertmanager/cli/format" "github.com/prometheus/alertmanager/client" ) const configHelp = `View current config. The amount of output is controlled by the output selection flag: - Simple: Print just the running config - Extended: Print the running config as well as uptime and all version info - Json: Print entire config object as json ` // configCmd represents the config command func configureConfigCmd(app *kingpin.Application) { app.Command("config", configHelp).Action(queryConfig).PreAction(requireAlertManagerURL) } func queryConfig(ctx *kingpin.ParseContext) error { c, err := api.NewClient(api.Config{Address: alertmanagerURL.String()}) if err != nil { return err } statusAPI := client.NewStatusAPI(c) status, err := statusAPI.Get(context.Background()) if err != nil { return err } formatter, found := format.Formatters[output] if !found { return errors.New("unknown output formatter") } return formatter.FormatConfig(status) } prometheus-alertmanager-0.15.3+ds/cli/config/000077500000000000000000000000001341674552200210625ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/cli/config/config.go000066400000000000000000000040131341674552200226540ustar00rootroot00000000000000// Copyright 2018 Prometheus Team // 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 config import ( "io/ioutil" "os" "gopkg.in/alecthomas/kingpin.v2" "gopkg.in/yaml.v2" ) type getFlagger interface { GetFlag(name string) *kingpin.FlagClause } // Resolver represents a configuration file resolver for kingpin. type Resolver struct { flags map[string]string } // NewResolver returns a Resolver structure. func NewResolver(files []string, legacyFlags map[string]string) (*Resolver, error) { flags := map[string]string{} for _, f := range files { b, err := ioutil.ReadFile(f) if err != nil { if os.IsNotExist(err) { continue } return nil, err } var m map[string]string err = yaml.Unmarshal(b, &m) if err != nil { return nil, err } for k, v := range m { if flag, ok := legacyFlags[k]; ok { if _, ok := m[flag]; ok { continue } k = flag } if _, ok := flags[k]; !ok { flags[k] = v } } } return &Resolver{flags: flags}, nil } func (c *Resolver) setDefault(v getFlagger) { for name, value := range c.flags { f := v.GetFlag(name) if f != nil { f.Default(value) } } } // Bind sets active flags with their default values from the configuration file(s). func (c *Resolver) Bind(app *kingpin.Application, args []string) error { // Parse the command line arguments to get the selected command. pc, err := app.ParseContext(args) if err != nil { return err } c.setDefault(app) if pc.SelectedCommand != nil { c.setDefault(pc.SelectedCommand) } return nil } prometheus-alertmanager-0.15.3+ds/cli/config/config_test.go000066400000000000000000000102361341674552200237170ustar00rootroot00000000000000// Copyright 2015 Prometheus Team // 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 config import ( "io/ioutil" "testing" "gopkg.in/alecthomas/kingpin.v2" ) var ( url *string id *string ) func newApp() *kingpin.Application { url = new(string) id = new(string) app := kingpin.New("app", "") app.UsageWriter(ioutil.Discard) app.ErrorWriter(ioutil.Discard) app.Terminate(nil) app.Flag("url", "").StringVar(url) silence := app.Command("silence", "") silenceDel := silence.Command("del", "") silenceDel.Flag("id", "").StringVar(id) return app } func TestNewConfigResolver(t *testing.T) { for i, tc := range []struct { files []string err bool }{ {[]string{}, false}, {[]string{"testdata/amtool.good1.yml", "testdata/amtool.good2.yml"}, false}, {[]string{"testdata/amtool.good1.yml", "testdata/not_existing.yml"}, false}, {[]string{"testdata/amtool.good1.yml", "testdata/amtool.bad.yml"}, true}, } { _, err := NewResolver(tc.files, nil) if tc.err != (err != nil) { if tc.err { t.Fatalf("%d: expected error but got none", i) } else { t.Fatalf("%d: expected no error but got %v", i, err) } } } } type expectFn func() func TestConfigResolverBind(t *testing.T) { expectURL := func(expected string) expectFn { return func() { if *url != expected { t.Fatalf("expected url flag %q but got %q", expected, *url) } } } expectID := func(expected string) expectFn { return func() { if *id != expected { t.Fatalf("expected ID flag %q but got %q", expected, *id) } } } for i, tc := range []struct { files []string legacyFlags map[string]string args []string err bool expCmd string expFns []expectFn }{ { []string{"testdata/amtool.good1.yml", "testdata/amtool.good2.yml"}, nil, []string{}, true, "", []expectFn{expectURL("url1")}, // from amtool.good1.yml }, { []string{"testdata/amtool.good2.yml"}, nil, []string{}, true, "", []expectFn{expectURL("url2")}, // from amtool.good2.yml }, { []string{"testdata/amtool.good1.yml", "testdata/amtool.good2.yml"}, nil, []string{"--url", "url3"}, true, "", []expectFn{expectURL("url3")}, // from command line }, { []string{"testdata/amtool.good1.yml", "testdata/amtool.good2.yml"}, map[string]string{"old-id": "id"}, []string{"silence", "del"}, false, "silence del", []expectFn{ expectURL("url1"), // from amtool.good1.yml expectID("id1"), // from amtool.good1.yml }, }, { []string{"testdata/amtool.good2.yml"}, map[string]string{"old-id": "id"}, []string{"silence", "del"}, false, "silence del", []expectFn{ expectURL("url2"), // from amtool.good2.yml expectID("id2"), // from amtool.good2.yml }, }, { []string{"testdata/amtool.good2.yml"}, map[string]string{"old-id": "id"}, []string{"silence", "del", "--id", "id3"}, false, "silence del", []expectFn{ expectURL("url2"), // from amtool.good2.yml expectID("id3"), // from command line }, }, } { r, err := NewResolver(tc.files, tc.legacyFlags) if err != nil { t.Fatalf("%d: expected no error but got: %v", i, err) } app := newApp() err = r.Bind(app, tc.args) if err != nil { t.Fatalf("%d: expected Bind() to return no error but got: %v", i, err) } cmd, err := app.Parse(tc.args) if tc.err != (err != nil) { if tc.err { t.Fatalf("%d: expected Parse() to return an error but got none", i) } else { t.Fatalf("%d: expected Parse() to return no error but got: %v", i, err) } } if cmd != tc.expCmd { t.Fatalf("%d: expected command %q but got %q", i, tc.expCmd, cmd) } for _, fn := range tc.expFns { fn() } } } prometheus-alertmanager-0.15.3+ds/cli/config/testdata/000077500000000000000000000000001341674552200226735ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/cli/config/testdata/amtool.bad.yml000066400000000000000000000000041341674552200254300ustar00rootroot00000000000000BAD prometheus-alertmanager-0.15.3+ds/cli/config/testdata/amtool.good1.yml000066400000000000000000000000221341674552200257130ustar00rootroot00000000000000id: id1 url: url1 prometheus-alertmanager-0.15.3+ds/cli/config/testdata/amtool.good2.yml000066400000000000000000000000261341674552200257200ustar00rootroot00000000000000old-id: id2 url: url2 prometheus-alertmanager-0.15.3+ds/cli/format/000077500000000000000000000000001341674552200211055ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/cli/format/format.go000066400000000000000000000026231341674552200227270ustar00rootroot00000000000000// Copyright 2018 Prometheus Team // 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 format import ( "io" "time" "gopkg.in/alecthomas/kingpin.v2" "github.com/prometheus/alertmanager/client" "github.com/prometheus/alertmanager/types" ) const DefaultDateFormat = "2006-01-02 15:04:05 MST" var ( dateFormat *string ) func InitFormatFlags(app *kingpin.Application) { dateFormat = app.Flag("date.format", "Format of date output").Default(DefaultDateFormat).String() } // Formatter needs to be implemented for each new output formatter. type Formatter interface { SetOutput(io.Writer) FormatSilences([]types.Silence) error FormatAlerts([]*client.ExtendedAlert) error FormatConfig(*client.ServerStatus) error } // Formatters is a map of cli argument names to formatter interface object. var Formatters = map[string]Formatter{} func FormatDate(input time.Time) string { return input.Format(*dateFormat) } prometheus-alertmanager-0.15.3+ds/cli/format/format_extended.go000066400000000000000000000071561341674552200246150ustar00rootroot00000000000000// Copyright 2018 Prometheus Team // 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 format import ( "fmt" "io" "os" "sort" "strings" "text/tabwriter" "github.com/prometheus/alertmanager/client" "github.com/prometheus/alertmanager/types" ) type ExtendedFormatter struct { writer io.Writer } func init() { Formatters["extended"] = &ExtendedFormatter{writer: os.Stdout} } func (formatter *ExtendedFormatter) SetOutput(writer io.Writer) { formatter.writer = writer } func (formatter *ExtendedFormatter) FormatSilences(silences []types.Silence) error { w := tabwriter.NewWriter(formatter.writer, 0, 0, 2, ' ', 0) sort.Sort(ByEndAt(silences)) fmt.Fprintln(w, "ID\tMatchers\tStarts At\tEnds At\tUpdated At\tCreated By\tComment\t") for _, silence := range silences { fmt.Fprintf( w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\t\n", silence.ID, extendedFormatMatchers(silence.Matchers), FormatDate(silence.StartsAt), FormatDate(silence.EndsAt), FormatDate(silence.UpdatedAt), silence.CreatedBy, silence.Comment, ) } return w.Flush() } func (formatter *ExtendedFormatter) FormatAlerts(alerts []*client.ExtendedAlert) error { w := tabwriter.NewWriter(formatter.writer, 0, 0, 2, ' ', 0) sort.Sort(ByStartsAt(alerts)) fmt.Fprintln(w, "Labels\tAnnotations\tStarts At\tEnds At\tGenerator URL\t") for _, alert := range alerts { fmt.Fprintf( w, "%s\t%s\t%s\t%s\t%s\t\n", extendedFormatLabels(alert.Labels), extendedFormatAnnotations(alert.Annotations), FormatDate(alert.StartsAt), FormatDate(alert.EndsAt), alert.GeneratorURL, ) } return w.Flush() } func (formatter *ExtendedFormatter) FormatConfig(status *client.ServerStatus) error { fmt.Fprintln(formatter.writer, status.ConfigYAML) fmt.Fprintln(formatter.writer, "buildUser", status.VersionInfo["buildUser"]) fmt.Fprintln(formatter.writer, "goVersion", status.VersionInfo["goVersion"]) fmt.Fprintln(formatter.writer, "revision", status.VersionInfo["revision"]) fmt.Fprintln(formatter.writer, "version", status.VersionInfo["version"]) fmt.Fprintln(formatter.writer, "branch", status.VersionInfo["branch"]) fmt.Fprintln(formatter.writer, "buildDate", status.VersionInfo["buildDate"]) fmt.Fprintln(formatter.writer, "uptime", status.Uptime) return nil } func extendedFormatLabels(labels client.LabelSet) string { output := []string{} for name, value := range labels { output = append(output, fmt.Sprintf("%s=\"%s\"", name, value)) } sort.Strings(output) return strings.Join(output, " ") } func extendedFormatAnnotations(labels client.LabelSet) string { output := []string{} for name, value := range labels { output = append(output, fmt.Sprintf("%s=\"%s\"", name, value)) } sort.Strings(output) return strings.Join(output, " ") } func extendedFormatMatchers(matchers types.Matchers) string { output := []string{} for _, matcher := range matchers { output = append(output, extendedFormatMatcher(*matcher)) } return strings.Join(output, " ") } func extendedFormatMatcher(matcher types.Matcher) string { if matcher.IsRegex { return fmt.Sprintf("%s~=%s", matcher.Name, matcher.Value) } return fmt.Sprintf("%s=%s", matcher.Name, matcher.Value) } prometheus-alertmanager-0.15.3+ds/cli/format/format_json.go000066400000000000000000000026041341674552200237570ustar00rootroot00000000000000// Copyright 2018 Prometheus Team // 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 format import ( "encoding/json" "io" "os" "github.com/prometheus/alertmanager/client" "github.com/prometheus/alertmanager/types" ) type JSONFormatter struct { writer io.Writer } func init() { Formatters["json"] = &JSONFormatter{writer: os.Stdout} } func (formatter *JSONFormatter) SetOutput(writer io.Writer) { formatter.writer = writer } func (formatter *JSONFormatter) FormatSilences(silences []types.Silence) error { enc := json.NewEncoder(formatter.writer) return enc.Encode(silences) } func (formatter *JSONFormatter) FormatAlerts(alerts []*client.ExtendedAlert) error { enc := json.NewEncoder(formatter.writer) return enc.Encode(alerts) } func (formatter *JSONFormatter) FormatConfig(status *client.ServerStatus) error { enc := json.NewEncoder(formatter.writer) return enc.Encode(status) } prometheus-alertmanager-0.15.3+ds/cli/format/format_simple.go000066400000000000000000000046341341674552200243040ustar00rootroot00000000000000// Copyright 2018 Prometheus Team // 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 format import ( "fmt" "io" "os" "sort" "strings" "text/tabwriter" "github.com/prometheus/alertmanager/client" "github.com/prometheus/alertmanager/types" ) type SimpleFormatter struct { writer io.Writer } func init() { Formatters["simple"] = &SimpleFormatter{writer: os.Stdout} } func (formatter *SimpleFormatter) SetOutput(writer io.Writer) { formatter.writer = writer } func (formatter *SimpleFormatter) FormatSilences(silences []types.Silence) error { w := tabwriter.NewWriter(formatter.writer, 0, 0, 2, ' ', 0) sort.Sort(ByEndAt(silences)) fmt.Fprintln(w, "ID\tMatchers\tEnds At\tCreated By\tComment\t") for _, silence := range silences { fmt.Fprintf( w, "%s\t%s\t%s\t%s\t%s\t\n", silence.ID, simpleFormatMatchers(silence.Matchers), FormatDate(silence.EndsAt), silence.CreatedBy, silence.Comment, ) } return w.Flush() } func (formatter *SimpleFormatter) FormatAlerts(alerts []*client.ExtendedAlert) error { w := tabwriter.NewWriter(formatter.writer, 0, 0, 2, ' ', 0) sort.Sort(ByStartsAt(alerts)) fmt.Fprintln(w, "Alertname\tStarts At\tSummary\t") for _, alert := range alerts { fmt.Fprintf( w, "%s\t%s\t%s\t\n", alert.Labels["alertname"], FormatDate(alert.StartsAt), alert.Annotations["summary"], ) } return w.Flush() } func (formatter *SimpleFormatter) FormatConfig(status *client.ServerStatus) error { fmt.Fprintln(formatter.writer, status.ConfigYAML) return nil } func simpleFormatMatchers(matchers types.Matchers) string { output := []string{} for _, matcher := range matchers { output = append(output, simpleFormatMatcher(*matcher)) } return strings.Join(output, " ") } func simpleFormatMatcher(matcher types.Matcher) string { if matcher.IsRegex { return fmt.Sprintf("%s=~%s", matcher.Name, matcher.Value) } return fmt.Sprintf("%s=%s", matcher.Name, matcher.Value) } prometheus-alertmanager-0.15.3+ds/cli/format/sort.go000066400000000000000000000022441341674552200224250ustar00rootroot00000000000000// Copyright 2018 Prometheus Team // 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 format import ( "github.com/prometheus/alertmanager/client" "github.com/prometheus/alertmanager/types" ) type ByEndAt []types.Silence func (s ByEndAt) Len() int { return len(s) } func (s ByEndAt) Swap(i, j int) { s[i], s[j] = s[j], s[i] } func (s ByEndAt) Less(i, j int) bool { return s[i].EndsAt.Before(s[j].EndsAt) } type ByStartsAt []*client.ExtendedAlert func (s ByStartsAt) Len() int { return len(s) } func (s ByStartsAt) Swap(i, j int) { s[i], s[j] = s[j], s[i] } func (s ByStartsAt) Less(i, j int) bool { return s[i].StartsAt.Before(s[j].StartsAt) } prometheus-alertmanager-0.15.3+ds/cli/root.go000066400000000000000000000062761341674552200211420ustar00rootroot00000000000000// Copyright 2018 Prometheus Team // 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 cli import ( "net/url" "os" "github.com/prometheus/common/version" "gopkg.in/alecthomas/kingpin.v2" "github.com/prometheus/alertmanager/cli/config" "github.com/prometheus/alertmanager/cli/format" ) var ( verbose bool alertmanagerURL *url.URL output string configFiles = []string{os.ExpandEnv("$HOME/.config/amtool/config.yml"), "/etc/amtool/config.yml"} legacyFlags = map[string]string{"comment_required": "require-comment"} ) func requireAlertManagerURL(pc *kingpin.ParseContext) error { // Return without error if any help flag is set. for _, elem := range pc.Elements { f, ok := elem.Clause.(*kingpin.FlagClause) if !ok { continue } name := f.Model().Name if name == "help" || name == "help-long" || name == "help-man" { return nil } } if alertmanagerURL == nil { kingpin.Fatalf("required flag --alertmanager.url not provided") } return nil } func Execute() { var ( app = kingpin.New("amtool", helpRoot).DefaultEnvars() ) format.InitFormatFlags(app) app.Flag("verbose", "Verbose running information").Short('v').BoolVar(&verbose) app.Flag("alertmanager.url", "Alertmanager to talk to").URLVar(&alertmanagerURL) app.Flag("output", "Output formatter (simple, extended, json)").Short('o').Default("simple").EnumVar(&output, "simple", "extended", "json") app.Version(version.Print("amtool")) app.GetFlag("help").Short('h') app.UsageTemplate(kingpin.CompactUsageTemplate) resolver, err := config.NewResolver(configFiles, legacyFlags) if err != nil { kingpin.Fatalf("could not load config file: %v\n", err) } configureAlertCmd(app) configureSilenceCmd(app) configureCheckConfigCmd(app) configureConfigCmd(app) err = resolver.Bind(app, os.Args[1:]) if err != nil { kingpin.Fatalf("%v\n", err) } _, err = app.Parse(os.Args[1:]) if err != nil { kingpin.Fatalf("%v\n", err) } } const ( helpRoot = `View and modify the current Alertmanager state. Config File: The alertmanager tool will read a config file in YAML format from one of two default config locations: $HOME/.config/amtool/config.yml or /etc/amtool/config.yml All flags can be given in the config file, but the following are the suited for static configuration: alertmanager.url Set a default alertmanager url for each request author Set a default author value for new silences. If this argument is not specified then the username will be used require-comment Bool, whether to require a comment on silence creation. Defaults to true output Set a default output type. Options are (simple, extended, json) date.format Sets the output format for dates. Defaults to "2006-01-02 15:04:05 MST" ` ) prometheus-alertmanager-0.15.3+ds/cli/silence.go000066400000000000000000000021051341674552200215640ustar00rootroot00000000000000// Copyright 2018 Prometheus Team // 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 cli import "gopkg.in/alecthomas/kingpin.v2" // silenceCmd represents the silence command func configureSilenceCmd(app *kingpin.Application) { silenceCmd := app.Command("silence", "Add, expire or view silences. For more information and additional flags see query help").PreAction(requireAlertManagerURL) configureSilenceAddCmd(silenceCmd) configureSilenceExpireCmd(silenceCmd) configureSilenceImportCmd(silenceCmd) configureSilenceQueryCmd(silenceCmd) configureSilenceUpdateCmd(silenceCmd) } prometheus-alertmanager-0.15.3+ds/cli/silence_add.go000066400000000000000000000105611341674552200224010ustar00rootroot00000000000000// Copyright 2018 Prometheus Team // 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 cli import ( "context" "errors" "fmt" "os/user" "time" "github.com/prometheus/client_golang/api" "github.com/prometheus/common/model" "gopkg.in/alecthomas/kingpin.v2" "github.com/prometheus/alertmanager/client" "github.com/prometheus/alertmanager/types" ) func username() string { user, err := user.Current() if err != nil { return "" } return user.Username } type silenceAddCmd struct { author string requireComment bool duration string start string end string comment string matchers []string } const silenceAddHelp = `Add a new alertmanager silence Amtool uses a simplified Prometheus syntax to represent silences. The non-option section of arguments constructs a list of "Matcher Groups" that will be used to create a number of silences. The following examples will attempt to show this behaviour in action: amtool silence add alertname=foo node=bar This statement will add a silence that matches alerts with the alertname=foo and node=bar label value pairs set. amtool silence add foo node=bar If alertname is omitted and the first argument does not contain a '=' or a '=~' then it will be assumed to be the value of the alertname pair. amtool silence add 'alertname=~foo.*' As well as direct equality, regex matching is also supported. The '=~' syntax (similar to Prometheus) is used to represent a regex match. Regex matching can be used in combination with a direct match. ` func configureSilenceAddCmd(cc *kingpin.CmdClause) { var ( c = &silenceAddCmd{} addCmd = cc.Command("add", silenceAddHelp) ) addCmd.Flag("author", "Username for CreatedBy field").Short('a').Default(username()).StringVar(&c.author) addCmd.Flag("require-comment", "Require comment to be set").Hidden().Default("true").BoolVar(&c.requireComment) addCmd.Flag("duration", "Duration of silence").Short('d').Default("1h").StringVar(&c.duration) addCmd.Flag("start", "Set when the silence should start. RFC3339 format 2006-01-02T15:04:05Z07:00").StringVar(&c.start) addCmd.Flag("end", "Set when the silence should end (overwrites duration). RFC3339 format 2006-01-02T15:04:05Z07:00").StringVar(&c.end) addCmd.Flag("comment", "A comment to help describe the silence").Short('c').StringVar(&c.comment) addCmd.Arg("matcher-groups", "Query filter").StringsVar(&c.matchers) addCmd.Action(c.add) } func (c *silenceAddCmd) add(ctx *kingpin.ParseContext) error { var err error matchers, err := parseMatchers(c.matchers) if err != nil { return err } if len(matchers) < 1 { return fmt.Errorf("no matchers specified") } var endsAt time.Time if c.end != "" { endsAt, err = time.Parse(time.RFC3339, c.end) if err != nil { return err } } else { d, err := model.ParseDuration(c.duration) if err != nil { return err } if d == 0 { return fmt.Errorf("silence duration must be greater than 0") } endsAt = time.Now().UTC().Add(time.Duration(d)) } if c.requireComment && c.comment == "" { return errors.New("comment required by config") } var startsAt time.Time if c.start != "" { startsAt, err = time.Parse(time.RFC3339, c.start) if err != nil { return err } } else { startsAt = time.Now().UTC() } if startsAt.After(endsAt) { return errors.New("silence cannot start after it ends") } typeMatchers, err := TypeMatchers(matchers) if err != nil { return err } silence := types.Silence{ Matchers: typeMatchers, StartsAt: startsAt, EndsAt: endsAt, CreatedBy: c.author, Comment: c.comment, } apiClient, err := api.NewClient(api.Config{Address: alertmanagerURL.String()}) if err != nil { return err } silenceAPI := client.NewSilenceAPI(apiClient) silenceID, err := silenceAPI.Set(context.Background(), silence) if err != nil { return err } _, err = fmt.Println(silenceID) return err } prometheus-alertmanager-0.15.3+ds/cli/silence_expire.go000066400000000000000000000027601341674552200231470ustar00rootroot00000000000000// Copyright 2018 Prometheus Team // 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 cli import ( "context" "errors" "github.com/prometheus/client_golang/api" "gopkg.in/alecthomas/kingpin.v2" "github.com/prometheus/alertmanager/client" ) type silenceExpireCmd struct { ids []string } func configureSilenceExpireCmd(cc *kingpin.CmdClause) { var ( c = &silenceExpireCmd{} expireCmd = cc.Command("expire", "expire an alertmanager silence") ) expireCmd.Arg("silence-ids", "Ids of silences to expire").StringsVar(&c.ids) expireCmd.Action(c.expire) } func (c *silenceExpireCmd) expire(ctx *kingpin.ParseContext) error { if len(c.ids) < 1 { return errors.New("no silence IDs specified") } apiClient, err := api.NewClient(api.Config{Address: alertmanagerURL.String()}) if err != nil { return err } silenceAPI := client.NewSilenceAPI(apiClient) for _, id := range c.ids { err := silenceAPI.Expire(context.Background(), id) if err != nil { return err } } return nil } prometheus-alertmanager-0.15.3+ds/cli/silence_import.go000066400000000000000000000071351341674552200231660ustar00rootroot00000000000000// Copyright 2018 Prometheus Team // 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 cli import ( "context" "encoding/json" "fmt" "os" "strings" "sync" "github.com/pkg/errors" "github.com/prometheus/client_golang/api" "gopkg.in/alecthomas/kingpin.v2" "github.com/prometheus/alertmanager/client" "github.com/prometheus/alertmanager/types" ) type silenceImportCmd struct { force bool workers int file string } const silenceImportHelp = `Import alertmanager silences from JSON file or stdin This command can be used to bulk import silences from a JSON file created by query command. For example: amtool silence query -o json foo > foo.json amtool silence import foo.json JSON data can also come from stdin if no param is specified. ` func configureSilenceImportCmd(cc *kingpin.CmdClause) { var ( c = &silenceImportCmd{} importCmd = cc.Command("import", silenceImportHelp) ) importCmd.Flag("force", "Force adding new silences even if it already exists").Short('f').BoolVar(&c.force) importCmd.Flag("worker", "Number of concurrent workers to use for import").Short('w').Default("8").IntVar(&c.workers) importCmd.Arg("input-file", "JSON file with silences").ExistingFileVar(&c.file) importCmd.Action(c.bulkImport) } func addSilenceWorker(sclient client.SilenceAPI, silencec <-chan *types.Silence, errc chan<- error) { for s := range silencec { silenceID, err := sclient.Set(context.Background(), *s) sid := s.ID if err != nil && strings.Contains(err.Error(), "not found") { // silence doesn't exists yet, retry to create as a new one s.ID = "" silenceID, err = sclient.Set(context.Background(), *s) } if err != nil { fmt.Fprintf(os.Stderr, "Error adding silence id='%v': %v\n", sid, err) } else { fmt.Println(silenceID) } errc <- err } } func (c *silenceImportCmd) bulkImport(ctx *kingpin.ParseContext) error { input := os.Stdin var err error if c.file != "" { input, err = os.Open(c.file) if err != nil { return err } defer input.Close() } dec := json.NewDecoder(input) // read open square bracket _, err = dec.Token() if err != nil { return errors.Wrap(err, "couldn't unmarshal input data, is it JSON?") } apiClient, err := api.NewClient(api.Config{Address: alertmanagerURL.String()}) if err != nil { return err } silenceAPI := client.NewSilenceAPI(apiClient) silencec := make(chan *types.Silence, 100) errc := make(chan error, 100) var wg sync.WaitGroup for w := 0; w < c.workers; w++ { wg.Add(1) go func() { addSilenceWorker(silenceAPI, silencec, errc) wg.Done() }() } errCount := 0 go func() { for err := range errc { if err != nil { errCount++ } } }() count := 0 for dec.More() { var s types.Silence err := dec.Decode(&s) if err != nil { return errors.Wrap(err, "couldn't unmarshal input data, is it JSON?") } if c.force { // reset the silence ID so Alertmanager will always create new silence s.ID = "" } silencec <- &s count++ } close(silencec) wg.Wait() close(errc) if errCount > 0 { return fmt.Errorf("couldn't import %v out of %v silences", errCount, count) } return nil } prometheus-alertmanager-0.15.3+ds/cli/silence_query.go000066400000000000000000000120451341674552200230150ustar00rootroot00000000000000// Copyright 2018 Prometheus Team // 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 cli import ( "context" "errors" "fmt" "strings" "time" "github.com/prometheus/client_golang/api" "gopkg.in/alecthomas/kingpin.v2" "github.com/prometheus/alertmanager/cli/format" "github.com/prometheus/alertmanager/client" "github.com/prometheus/alertmanager/pkg/parse" "github.com/prometheus/alertmanager/types" ) type silenceQueryCmd struct { expired bool quiet bool matchers []string within time.Duration } const querySilenceHelp = `Query Alertmanager silences. Amtool has a simplified prometheus query syntax, but contains robust support for bash variable expansions. The non-option section of arguments constructs a list of "Matcher Groups" that will be used to filter your query. The following examples will attempt to show this behaviour in action: amtool silence query alertname=foo node=bar This query will match all silences with the alertname=foo and node=bar label value pairs set. amtool silence query foo node=bar If alertname is omitted and the first argument does not contain a '=' or a '=~' then it will be assumed to be the value of the alertname pair. amtool silence query 'alertname=~foo.*' As well as direct equality, regex matching is also supported. The '=~' syntax (similar to prometheus) is used to represent a regex match. Regex matching can be used in combination with a direct match. In addition to filtering by silence labels, one can also query for silences that are due to expire soon with the "--within" parameter. In the event that you want to preemptively act upon expiring silences by either fixing them or extending them. For example: amtool silence query --within 8h returns all the silences due to expire within the next 8 hours. This syntax can also be combined with the label based filtering above for more flexibility. The "--expired" parameter returns only expired silences. Used in combination with "--within=TIME", amtool returns the silences that expired within the preceding duration. amtool silence query --within 2h --expired returns all silences that expired within the preceeding 2 hours. ` func configureSilenceQueryCmd(cc *kingpin.CmdClause) { var ( c = &silenceQueryCmd{} queryCmd = cc.Command("query", querySilenceHelp).Default() ) queryCmd.Flag("expired", "Show expired silences instead of active").BoolVar(&c.expired) queryCmd.Flag("quiet", "Only show silence ids").Short('q').BoolVar(&c.quiet) queryCmd.Arg("matcher-groups", "Query filter").StringsVar(&c.matchers) queryCmd.Flag("within", "Show silences that will expire or have expired within a duration").DurationVar(&c.within) queryCmd.Action(c.query) } func (c *silenceQueryCmd) query(ctx *kingpin.ParseContext) error { var filterString = "" if len(c.matchers) == 1 { // If the parser fails then we likely don't have a (=|=~|!=|!~) so lets // assume that the user wants alertname= and prepend `alertname=` // to the front. _, err := parse.Matcher(c.matchers[0]) if err != nil { filterString = fmt.Sprintf("{alertname=%s}", c.matchers[0]) } else { filterString = fmt.Sprintf("{%s}", strings.Join(c.matchers, ",")) } } else if len(c.matchers) > 1 { filterString = fmt.Sprintf("{%s}", strings.Join(c.matchers, ",")) } apiClient, err := api.NewClient(api.Config{Address: alertmanagerURL.String()}) if err != nil { return err } silenceAPI := client.NewSilenceAPI(apiClient) fetchedSilences, err := silenceAPI.List(context.Background(), filterString) if err != nil { return err } displaySilences := []types.Silence{} for _, silence := range fetchedSilences { // skip expired silences if --expired is not set if !c.expired && silence.EndsAt.Before(time.Now()) { continue } // skip active silences if --expired is set if c.expired && silence.EndsAt.After(time.Now()) { continue } // skip active silences expiring after "--within" if !c.expired && int64(c.within) > 0 && silence.EndsAt.After(time.Now().UTC().Add(c.within)) { continue } // skip silences that expired before "--within" if c.expired && int64(c.within) > 0 && silence.EndsAt.Before(time.Now().UTC().Add(-c.within)) { continue } displaySilences = append(displaySilences, *silence) } if c.quiet { for _, silence := range displaySilences { fmt.Println(silence.ID) } } else { formatter, found := format.Formatters[output] if !found { return errors.New("unknown output formatter") } if err := formatter.FormatSilences(displaySilences); err != nil { return fmt.Errorf("error formatting silences: %v", err) } } return nil } prometheus-alertmanager-0.15.3+ds/cli/silence_update.go000066400000000000000000000067051341674552200231400ustar00rootroot00000000000000// Copyright 2018 Prometheus Team // 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 cli import ( "context" "errors" "fmt" "time" "github.com/prometheus/client_golang/api" "gopkg.in/alecthomas/kingpin.v2" "github.com/prometheus/alertmanager/cli/format" "github.com/prometheus/alertmanager/client" "github.com/prometheus/alertmanager/types" "github.com/prometheus/common/model" ) type silenceUpdateCmd struct { quiet bool duration string start string end string comment string ids []string } func configureSilenceUpdateCmd(cc *kingpin.CmdClause) { var ( c = &silenceUpdateCmd{} updateCmd = cc.Command("update", "Update silences") ) updateCmd.Flag("quiet", "Only show silence ids").Short('q').BoolVar(&c.quiet) updateCmd.Flag("duration", "Duration of silence").Short('d').StringVar(&c.duration) updateCmd.Flag("start", "Set when the silence should start. RFC3339 format 2006-01-02T15:04:05Z07:00").StringVar(&c.start) updateCmd.Flag("end", "Set when the silence should end (overwrites duration). RFC3339 format 2006-01-02T15:04:05Z07:00").StringVar(&c.end) updateCmd.Flag("comment", "A comment to help describe the silence").Short('c').StringVar(&c.comment) updateCmd.Arg("update-ids", "Silence IDs to update").StringsVar(&c.ids) updateCmd.Action(c.update) } func (c *silenceUpdateCmd) update(ctx *kingpin.ParseContext) error { if len(c.ids) < 1 { return fmt.Errorf("no silence IDs specified") } apiClient, err := api.NewClient(api.Config{Address: alertmanagerURL.String()}) if err != nil { return err } silenceAPI := client.NewSilenceAPI(apiClient) var updatedSilences []types.Silence for _, silenceID := range c.ids { silence, err := silenceAPI.Get(context.Background(), silenceID) if err != nil { return err } if c.start != "" { silence.StartsAt, err = time.Parse(time.RFC3339, c.start) if err != nil { return err } } if c.end != "" { silence.EndsAt, err = time.Parse(time.RFC3339, c.end) if err != nil { return err } } else if c.duration != "" { d, err := model.ParseDuration(c.duration) if err != nil { return err } if d == 0 { return fmt.Errorf("silence duration must be greater than 0") } silence.EndsAt = silence.StartsAt.UTC().Add(time.Duration(d)) } if silence.StartsAt.After(silence.EndsAt) { return errors.New("silence cannot start after it ends") } if c.comment != "" { silence.Comment = c.comment } newID, err := silenceAPI.Set(context.Background(), *silence) if err != nil { return err } silence.ID = newID updatedSilences = append(updatedSilences, *silence) } if c.quiet { for _, silence := range updatedSilences { fmt.Println(silence.ID) } } else { formatter, found := format.Formatters[output] if !found { return fmt.Errorf("unknown output formatter") } if err := formatter.FormatSilences(updatedSilences); err != nil { return fmt.Errorf("error formatting silences: %v", err) } } return nil } prometheus-alertmanager-0.15.3+ds/cli/testdata/000077500000000000000000000000001341674552200214265ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/cli/testdata/conf.bad.yml000066400000000000000000000000031341674552200236140ustar00rootroot00000000000000BADprometheus-alertmanager-0.15.3+ds/cli/testdata/conf.good.yml000066400000000000000000000002271341674552200240260ustar00rootroot00000000000000global: smtp_smarthost: 'localhost:25' templates: - '/etc/alertmanager/template/*.tmpl' route: receiver: default receivers: - name: default prometheus-alertmanager-0.15.3+ds/cli/utils.go000066400000000000000000000051771341674552200213160ustar00rootroot00000000000000// Copyright 2018 Prometheus Team // 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 cli import ( "fmt" "net/url" "path" "github.com/prometheus/alertmanager/pkg/parse" "github.com/prometheus/alertmanager/types" "github.com/prometheus/common/model" "github.com/prometheus/prometheus/pkg/labels" ) type ByAlphabetical []labels.Matcher func (s ByAlphabetical) Len() int { return len(s) } func (s ByAlphabetical) Swap(i, j int) { s[i], s[j] = s[j], s[i] } func (s ByAlphabetical) Less(i, j int) bool { if s[i].Name != s[j].Name { return s[i].Name < s[j].Name } else if s[i].Type != s[j].Type { return s[i].Type < s[j].Type } else if s[i].Value != s[j].Value { return s[i].Value < s[j].Value } return false } func GetAlertmanagerURL(p string) url.URL { amURL := *alertmanagerURL amURL.Path = path.Join(alertmanagerURL.Path, p) return amURL } // Parse a list of labels (cli arguments) func parseMatchers(inputLabels []string) ([]labels.Matcher, error) { matchers := make([]labels.Matcher, 0) for _, v := range inputLabels { name, value, matchType, err := parse.Input(v) if err != nil { return []labels.Matcher{}, err } matchers = append(matchers, labels.Matcher{ Type: matchType, Name: name, Value: value, }) } return matchers, nil } // Only valid for when you are going to add a silence func TypeMatchers(matchers []labels.Matcher) (types.Matchers, error) { typeMatchers := types.Matchers{} for _, matcher := range matchers { typeMatcher, err := TypeMatcher(matcher) if err != nil { return types.Matchers{}, err } typeMatchers = append(typeMatchers, &typeMatcher) } return typeMatchers, nil } // Only valid for when you are going to add a silence // Doesn't allow negative operators func TypeMatcher(matcher labels.Matcher) (types.Matcher, error) { typeMatcher := types.NewMatcher(model.LabelName(matcher.Name), matcher.Value) switch matcher.Type { case labels.MatchEqual: typeMatcher.IsRegex = false case labels.MatchRegexp: typeMatcher.IsRegex = true default: return types.Matcher{}, fmt.Errorf("invalid match type for creation operation: %s", matcher.Type) } return *typeMatcher, nil } prometheus-alertmanager-0.15.3+ds/client/000077500000000000000000000000001341674552200203245ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/client/client.go000066400000000000000000000222051341674552200221320ustar00rootroot00000000000000// 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. package client import ( "bytes" "context" "encoding/json" "fmt" "net/http" "net/url" "time" "github.com/prometheus/client_golang/api" "github.com/prometheus/alertmanager/config" "github.com/prometheus/alertmanager/types" ) const ( apiPrefix = "/api/v1" epStatus = apiPrefix + "/status" epSilence = apiPrefix + "/silence/:id" epSilences = apiPrefix + "/silences" epAlerts = apiPrefix + "/alerts" epAlertGroups = apiPrefix + "/alerts/groups" statusSuccess = "success" statusError = "error" ) // ServerStatus represents the status of the AlertManager endpoint. type ServerStatus struct { ConfigYAML string `json:"configYAML"` ConfigJSON *config.Config `json:"configJSON"` VersionInfo map[string]string `json:"versionInfo"` Uptime time.Time `json:"uptime"` ClusterStatus *ClusterStatus `json:"clusterStatus"` } // PeerStatus represents the status of a peer in the cluster. type PeerStatus struct { Name string `json:"name"` Address string `json:"address"` } // ClusterStatus represents the status of the cluster. type ClusterStatus struct { Name string `json:"name"` Status string `json:"status"` Peers []PeerStatus `json:"peers"` } // apiClient wraps a regular client and processes successful API responses. // Successful also includes responses that errored at the API level. type apiClient struct { api.Client } type apiResponse struct { Status string `json:"status"` Data json.RawMessage `json:"data,omitempty"` ErrorType string `json:"errorType,omitempty"` Error string `json:"error,omitempty"` } type clientError struct { code int msg string } func (e *clientError) Error() string { return fmt.Sprintf("%s (code: %d)", e.msg, e.code) } func (c apiClient) Do(ctx context.Context, req *http.Request) (*http.Response, []byte, error) { resp, body, err := c.Client.Do(ctx, req) if err != nil { return resp, body, err } code := resp.StatusCode var result apiResponse if err = json.Unmarshal(body, &result); err != nil { // Pass the returned body rather than the JSON error because some API // endpoints return plain text instead of JSON payload. return resp, body, &clientError{ code: code, msg: string(body), } } if (code/100 == 2) && (result.Status != statusSuccess) { return resp, body, &clientError{ code: code, msg: "inconsistent body for response code", } } if result.Status == statusError { err = &clientError{ code: code, msg: result.Error, } } return resp, []byte(result.Data), err } // StatusAPI provides bindings for the Alertmanager's status API. type StatusAPI interface { // Get returns the server's configuration, version, uptime and cluster information. Get(ctx context.Context) (*ServerStatus, error) } // NewStatusAPI returns a status API client. func NewStatusAPI(c api.Client) StatusAPI { return &httpStatusAPI{client: apiClient{c}} } type httpStatusAPI struct { client api.Client } func (h *httpStatusAPI) Get(ctx context.Context) (*ServerStatus, error) { u := h.client.URL(epStatus, nil) req, err := http.NewRequest(http.MethodGet, u.String(), nil) if err != nil { return nil, fmt.Errorf("error creating request: %v", err) } _, body, err := h.client.Do(ctx, req) if err != nil { return nil, err } var ss *ServerStatus err = json.Unmarshal(body, &ss) return ss, err } // AlertAPI provides bindings for the Alertmanager's alert API. type AlertAPI interface { // List returns all the active alerts. List(ctx context.Context, filter, receiver string, silenced, inhibited, active, unprocessed bool) ([]*ExtendedAlert, error) // Push sends a list of alerts to the Alertmanager. Push(ctx context.Context, alerts ...Alert) error } // Alert represents an alert as expected by the AlertManager's push alert API. type Alert struct { Labels LabelSet `json:"labels"` Annotations LabelSet `json:"annotations"` StartsAt time.Time `json:"startsAt,omitempty"` EndsAt time.Time `json:"endsAt,omitempty"` GeneratorURL string `json:"generatorURL"` } // ExtendedAlert represents an alert as returned by the AlertManager's list alert API. type ExtendedAlert struct { Alert Status types.AlertStatus `json:"status"` Receivers []string `json:"receivers"` Fingerprint string `json:"fingerprint"` } // LabelSet represents a collection of label names and values as a map. type LabelSet map[LabelName]LabelValue // LabelName represents the name of a label. type LabelName string // LabelValue represents the value of a label. type LabelValue string // NewAlertAPI returns a new AlertAPI for the client. func NewAlertAPI(c api.Client) AlertAPI { return &httpAlertAPI{client: apiClient{c}} } type httpAlertAPI struct { client api.Client } func (h *httpAlertAPI) List(ctx context.Context, filter, receiver string, silenced, inhibited, active, unprocessed bool) ([]*ExtendedAlert, error) { u := h.client.URL(epAlerts, nil) params := url.Values{} if filter != "" { params.Add("filter", filter) } params.Add("silenced", fmt.Sprintf("%t", silenced)) params.Add("inhibited", fmt.Sprintf("%t", inhibited)) params.Add("active", fmt.Sprintf("%t", active)) params.Add("unprocessed", fmt.Sprintf("%t", unprocessed)) params.Add("receiver", receiver) u.RawQuery = params.Encode() req, err := http.NewRequest(http.MethodGet, u.String(), nil) if err != nil { return nil, fmt.Errorf("error creating request: %v", err) } _, body, err := h.client.Do(ctx, req) if err != nil { return nil, err } var alts []*ExtendedAlert err = json.Unmarshal(body, &alts) return alts, err } func (h *httpAlertAPI) Push(ctx context.Context, alerts ...Alert) error { u := h.client.URL(epAlerts, nil) var buf bytes.Buffer if err := json.NewEncoder(&buf).Encode(&alerts); err != nil { return err } req, err := http.NewRequest(http.MethodPost, u.String(), &buf) if err != nil { return fmt.Errorf("error creating request: %v", err) } _, _, err = h.client.Do(ctx, req) return err } // SilenceAPI provides bindings for the Alertmanager's silence API. type SilenceAPI interface { // Get returns the silence associated with the given ID. Get(ctx context.Context, id string) (*types.Silence, error) // Set updates or creates the given silence and returns its ID. Set(ctx context.Context, sil types.Silence) (string, error) // Expire expires the silence with the given ID. Expire(ctx context.Context, id string) error // List returns silences matching the given filter. List(ctx context.Context, filter string) ([]*types.Silence, error) } // NewSilenceAPI returns a new SilenceAPI for the client. func NewSilenceAPI(c api.Client) SilenceAPI { return &httpSilenceAPI{client: apiClient{c}} } type httpSilenceAPI struct { client api.Client } func (h *httpSilenceAPI) Get(ctx context.Context, id string) (*types.Silence, error) { u := h.client.URL(epSilence, map[string]string{ "id": id, }) req, err := http.NewRequest(http.MethodGet, u.String(), nil) if err != nil { return nil, fmt.Errorf("error creating request: %v", err) } _, body, err := h.client.Do(ctx, req) if err != nil { return nil, err } var sil types.Silence err = json.Unmarshal(body, &sil) return &sil, err } func (h *httpSilenceAPI) Expire(ctx context.Context, id string) error { u := h.client.URL(epSilence, map[string]string{ "id": id, }) req, err := http.NewRequest(http.MethodDelete, u.String(), nil) if err != nil { return fmt.Errorf("error creating request: %v", err) } _, _, err = h.client.Do(ctx, req) return err } func (h *httpSilenceAPI) Set(ctx context.Context, sil types.Silence) (string, error) { u := h.client.URL(epSilences, nil) var buf bytes.Buffer if err := json.NewEncoder(&buf).Encode(&sil); err != nil { return "", err } req, err := http.NewRequest(http.MethodPost, u.String(), &buf) if err != nil { return "", fmt.Errorf("error creating request: %v", err) } _, body, err := h.client.Do(ctx, req) if err != nil { return "", err } var res struct { SilenceID string `json:"silenceId"` } err = json.Unmarshal(body, &res) return res.SilenceID, err } func (h *httpSilenceAPI) List(ctx context.Context, filter string) ([]*types.Silence, error) { u := h.client.URL(epSilences, nil) params := url.Values{} if filter != "" { params.Add("filter", filter) } u.RawQuery = params.Encode() req, err := http.NewRequest(http.MethodGet, u.String(), nil) if err != nil { return nil, fmt.Errorf("error creating request: %v", err) } _, body, err := h.client.Do(ctx, req) if err != nil { return nil, err } var sils []*types.Silence err = json.Unmarshal(body, &sils) return sils, err } prometheus-alertmanager-0.15.3+ds/client/client_test.go000066400000000000000000000235431341674552200231770ustar00rootroot00000000000000// 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. package client import ( "bytes" "context" "encoding/json" "fmt" "net/http" "net/url" "strings" "testing" "time" "github.com/prometheus/alertmanager/config" "github.com/prometheus/alertmanager/types" ) type apiTest struct { // Wrapper around the tested function. do func() (interface{}, error) apiRes fakeAPIResponse // Expected values returned by the tested function. res interface{} err error } // Fake HTTP client for TestAPI. type fakeAPIClient struct { *testing.T ch chan fakeAPIResponse } type fakeAPIResponse struct { // Expected input values. path string method string // Values to be returned by fakeAPIClient.Do(). err error res interface{} } func (c *fakeAPIClient) URL(ep string, args map[string]string) *url.URL { path := ep for k, v := range args { path = strings.Replace(path, ":"+k, v, -1) } return &url.URL{ Host: "test:9093", Path: path, } } func (c *fakeAPIClient) Do(ctx context.Context, req *http.Request) (*http.Response, []byte, error) { test := <-c.ch if req.URL.Path != test.path { c.Errorf("unexpected request path: want %s, got %s", test.path, req.URL.Path) } if req.Method != test.method { c.Errorf("unexpected request method: want %s, got %s", test.method, req.Method) } b, err := json.Marshal(test.res) if err != nil { c.Fatal(err) } return &http.Response{}, b, test.err } func TestAPI(t *testing.T) { client := &fakeAPIClient{T: t, ch: make(chan fakeAPIResponse, 1)} now := time.Now() statusData := &ServerStatus{ ConfigYAML: "{}", ConfigJSON: &config.Config{}, VersionInfo: map[string]string{"version": "v1"}, Uptime: now, ClusterStatus: &ClusterStatus{Peers: []PeerStatus{}}, } doStatus := func() (interface{}, error) { api := httpStatusAPI{client: client} return api.Get(context.Background()) } alertOne := Alert{ StartsAt: now, EndsAt: now.Add(time.Duration(5 * time.Minute)), Labels: LabelSet{"label1": "test1"}, Annotations: LabelSet{"annotation1": "some text"}, } alerts := []*ExtendedAlert{ { Alert: alertOne, Fingerprint: "1c93eec3511dc156", Status: types.AlertStatus{ State: types.AlertStateActive, }, }, } doAlertList := func() (interface{}, error) { api := httpAlertAPI{client: client} return api.List(context.Background(), "", "", false, false, false, false) } doAlertPush := func() (interface{}, error) { api := httpAlertAPI{client: client} return nil, api.Push(context.Background(), []Alert{alertOne}...) } silOne := &types.Silence{ ID: "abc", Matchers: []*types.Matcher{ { Name: "label1", Value: "test1", IsRegex: false, }, }, StartsAt: now, EndsAt: now.Add(time.Duration(2 * time.Hour)), UpdatedAt: now, CreatedBy: "alice", Comment: "some comment", Status: types.SilenceStatus{ State: "active", }, } doSilenceGet := func(id string) func() (interface{}, error) { return func() (interface{}, error) { api := httpSilenceAPI{client: client} return api.Get(context.Background(), id) } } doSilenceSet := func(sil types.Silence) func() (interface{}, error) { return func() (interface{}, error) { api := httpSilenceAPI{client: client} return api.Set(context.Background(), sil) } } doSilenceExpire := func(id string) func() (interface{}, error) { return func() (interface{}, error) { api := httpSilenceAPI{client: client} return nil, api.Expire(context.Background(), id) } } doSilenceList := func() (interface{}, error) { api := httpSilenceAPI{client: client} return api.List(context.Background(), "") } tests := []apiTest{ { do: doStatus, apiRes: fakeAPIResponse{ res: statusData, path: "/api/v1/status", method: http.MethodGet, }, res: statusData, }, { do: doStatus, apiRes: fakeAPIResponse{ err: fmt.Errorf("some error"), path: "/api/v1/status", method: http.MethodGet, }, err: fmt.Errorf("some error"), }, { do: doAlertList, apiRes: fakeAPIResponse{ res: alerts, path: "/api/v1/alerts", method: http.MethodGet, }, res: alerts, }, { do: doAlertList, apiRes: fakeAPIResponse{ err: fmt.Errorf("some error"), path: "/api/v1/alerts", method: http.MethodGet, }, err: fmt.Errorf("some error"), }, { do: doAlertPush, apiRes: fakeAPIResponse{ res: nil, path: "/api/v1/alerts", method: http.MethodPost, }, res: nil, }, { do: doAlertPush, apiRes: fakeAPIResponse{ err: fmt.Errorf("some error"), path: "/api/v1/alerts", method: http.MethodPost, }, err: fmt.Errorf("some error"), }, { do: doSilenceGet("abc"), apiRes: fakeAPIResponse{ res: silOne, path: "/api/v1/silence/abc", method: http.MethodGet, }, res: silOne, }, { do: doSilenceGet("abc"), apiRes: fakeAPIResponse{ err: fmt.Errorf("some error"), path: "/api/v1/silence/abc", method: http.MethodGet, }, err: fmt.Errorf("some error"), }, { do: doSilenceSet(*silOne), apiRes: fakeAPIResponse{ res: map[string]string{"SilenceId": "abc"}, path: "/api/v1/silences", method: http.MethodPost, }, res: "abc", }, { do: doSilenceSet(*silOne), apiRes: fakeAPIResponse{ err: fmt.Errorf("some error"), path: "/api/v1/silences", method: http.MethodPost, }, err: fmt.Errorf("some error"), }, { do: doSilenceExpire("abc"), apiRes: fakeAPIResponse{ path: "/api/v1/silence/abc", method: http.MethodDelete, }, }, { do: doSilenceExpire("abc"), apiRes: fakeAPIResponse{ err: fmt.Errorf("some error"), path: "/api/v1/silence/abc", method: http.MethodDelete, }, err: fmt.Errorf("some error"), }, { do: doSilenceList, apiRes: fakeAPIResponse{ res: []*types.Silence{silOne}, path: "/api/v1/silences", method: http.MethodGet, }, res: []*types.Silence{silOne}, }, { do: doSilenceList, apiRes: fakeAPIResponse{ err: fmt.Errorf("some error"), path: "/api/v1/silences", method: http.MethodGet, }, err: fmt.Errorf("some error"), }, } for _, test := range tests { test := test client.ch <- test.apiRes t.Run(fmt.Sprintf("%s %s", test.apiRes.method, test.apiRes.path), func(t *testing.T) { res, err := test.do() if test.err != nil { if err == nil { t.Errorf("unexpected error: want: %s but got none", test.err) return } if err.Error() != test.err.Error() { t.Errorf("unexpected error: want: %s, got: %s", test.err, err) } return } if err != nil { t.Errorf("unexpected error: %s", err) return } want, err := json.Marshal(test.res) if err != nil { t.Fatal(err) } got, err := json.Marshal(res) if err != nil { t.Fatal(err) } if bytes.Compare(want, got) != 0 { t.Errorf("unexpected result: want: %s, got: %s", string(want), string(got)) } }) } } // Fake HTTP client for TestAPIClientDo. type fakeClient struct { *testing.T ch chan fakeResponse } type fakeResponse struct { code int res interface{} err error } func (c fakeClient) URL(string, map[string]string) *url.URL { return nil } func (c fakeClient) Do(context.Context, *http.Request) (*http.Response, []byte, error) { fakeRes := <-c.ch if fakeRes.err != nil { return nil, nil, fakeRes.err } var b []byte var err error switch v := fakeRes.res.(type) { case string: b = []byte(v) default: b, err = json.Marshal(v) if err != nil { c.Fatal(err) } } return &http.Response{StatusCode: fakeRes.code}, b, nil } type apiClientTest struct { response fakeResponse expected string err error } func TestAPIClientDo(t *testing.T) { tests := []apiClientTest{ { response: fakeResponse{ code: http.StatusOK, res: &apiResponse{ Status: statusSuccess, Data: json.RawMessage(`"test"`), }, err: nil, }, expected: `"test"`, err: nil, }, { response: fakeResponse{ code: http.StatusBadRequest, res: &apiResponse{ Status: statusError, Error: "some error", }, err: nil, }, err: fmt.Errorf("some error (code: 400)"), }, { response: fakeResponse{ code: http.StatusOK, res: &apiResponse{ Status: statusError, Error: "some error", }, err: nil, }, err: fmt.Errorf("inconsistent body for response code (code: 200)"), }, { response: fakeResponse{ code: http.StatusNotFound, res: "not found", err: nil, }, err: fmt.Errorf("not found (code: 404)"), }, { response: fakeResponse{ err: fmt.Errorf("some error"), }, err: fmt.Errorf("some error"), }, } fake := fakeClient{T: t, ch: make(chan fakeResponse, 1)} client := apiClient{fake} for _, test := range tests { t.Run("", func(t *testing.T) { fake.ch <- test.response _, body, err := client.Do(context.Background(), &http.Request{}) if test.err != nil { if err == nil { t.Errorf("expected error %q but got none", test.err) return } if test.err.Error() != err.Error() { t.Errorf("unexpected error: want %q, got %q", test.err, err) return } return } if err != nil { t.Errorf("unexpected error %q", err) return } want, got := test.expected, string(body) if want != got { t.Errorf("unexpected body: want %q, got %q", want, got) } }) } } prometheus-alertmanager-0.15.3+ds/cluster/000077500000000000000000000000001341674552200205275ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/cluster/advertise.go000066400000000000000000000040701341674552200230450ustar00rootroot00000000000000// Copyright 2018 Prometheus Team // 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 cluster import ( "net" "github.com/hashicorp/go-sockaddr" "github.com/pkg/errors" ) type getPrivateIPFunc func() (string, error) // This is overriden in unit tests to mock the sockaddr.GetPrivateIP function. var getPrivateAddress getPrivateIPFunc = sockaddr.GetPrivateIP // calculateAdvertiseAddress attempts to clone logic from deep within memberlist // (NetTransport.FinalAdvertiseAddr) in order to surface its conclusions to the // application, so we can provide more actionable error messages if the user has // inadvertantly misconfigured their cluster. // // https://github.com/hashicorp/memberlist/blob/022f081/net_transport.go#L126 func calculateAdvertiseAddress(bindAddr, advertiseAddr string) (net.IP, error) { if advertiseAddr != "" { ip := net.ParseIP(advertiseAddr) if ip == nil { return nil, errors.Errorf("failed to parse advertise addr '%s'", advertiseAddr) } if ip4 := ip.To4(); ip4 != nil { ip = ip4 } return ip, nil } if isAny(bindAddr) { privateIP, err := getPrivateAddress() if err != nil { return nil, errors.Wrap(err, "failed to get private IP") } if privateIP == "" { return nil, errors.New("no private IP found, explicit advertise addr not provided") } ip := net.ParseIP(privateIP) if ip == nil { return nil, errors.Errorf("failed to parse private IP '%s'", privateIP) } return ip, nil } ip := net.ParseIP(bindAddr) if ip == nil { return nil, errors.Errorf("failed to parse bind addr '%s'", bindAddr) } return ip, nil } prometheus-alertmanager-0.15.3+ds/cluster/advertise_test.go000066400000000000000000000037441341674552200241130ustar00rootroot00000000000000// Copyright 2018 Prometheus Team // 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 cluster import ( "errors" "net" "testing" "github.com/stretchr/testify/require" ) func TestCalculateAdvertiseAddress(t *testing.T) { old := getPrivateAddress defer func() { getPrivateAddress = old }() cases := []struct { fn getPrivateIPFunc bind, advertise string expectedIP net.IP err bool }{ { bind: "192.0.2.1", advertise: "", expectedIP: net.ParseIP("192.0.2.1"), err: false, }, { bind: "192.0.2.1", advertise: "192.0.2.2", expectedIP: net.ParseIP("192.0.2.2"), err: false, }, { fn: func() (string, error) { return "192.0.2.1", nil }, bind: "0.0.0.0", advertise: "", expectedIP: net.ParseIP("192.0.2.1"), err: false, }, { fn: func() (string, error) { return "", errors.New("some error") }, bind: "0.0.0.0", advertise: "", err: true, }, { fn: func() (string, error) { return "invalid", nil }, bind: "0.0.0.0", advertise: "", err: true, }, { fn: func() (string, error) { return "", nil }, bind: "0.0.0.0", advertise: "", err: true, }, } for _, c := range cases { getPrivateAddress = c.fn got, err := calculateAdvertiseAddress(c.bind, c.advertise) if c.err { require.Error(t, err) } else { require.NoError(t, err) require.Equal(t, c.expectedIP.String(), got.String()) } } } prometheus-alertmanager-0.15.3+ds/cluster/channel.go000066400000000000000000000115261341674552200224730ustar00rootroot00000000000000// Copyright 2018 Prometheus Team // 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 cluster import ( "sync" "time" "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" "github.com/gogo/protobuf/proto" "github.com/hashicorp/memberlist" "github.com/prometheus/alertmanager/cluster/clusterpb" "github.com/prometheus/client_golang/prometheus" ) // Channel allows clients to send messages for a specific state type that will be // broadcasted in a best-effort manner. type Channel struct { key string send func([]byte) peers func() []*memberlist.Node sendOversize func(*memberlist.Node, []byte) error msgc chan []byte logger log.Logger oversizeGossipMessageFailureTotal prometheus.Counter oversizeGossipMessageDroppedTotal prometheus.Counter oversizeGossipMessageSentTotal prometheus.Counter oversizeGossipDuration prometheus.Histogram } // NewChannel creates a new Channel struct, which handles sending normal and // oversize messages to peers. func NewChannel( key string, send func([]byte), peers func() []*memberlist.Node, sendOversize func(*memberlist.Node, []byte) error, logger log.Logger, stopc chan struct{}, reg prometheus.Registerer, ) *Channel { oversizeGossipMessageFailureTotal := prometheus.NewCounter(prometheus.CounterOpts{ Name: "alertmanager_oversized_gossip_message_failure_total", Help: "Number of oversized gossip message sends that failed.", ConstLabels: prometheus.Labels{"key": key}, }) oversizeGossipMessageSentTotal := prometheus.NewCounter(prometheus.CounterOpts{ Name: "alertmanager_oversized_gossip_message_sent_total", Help: "Number of oversized gossip message sent.", ConstLabels: prometheus.Labels{"key": key}, }) oversizeGossipMessageDroppedTotal := prometheus.NewCounter(prometheus.CounterOpts{ Name: "alertmanager_oversized_gossip_message_dropped_total", Help: "Number of oversized gossip messages that were dropped due to a full message queue.", ConstLabels: prometheus.Labels{"key": key}, }) oversizeGossipDuration := prometheus.NewHistogram(prometheus.HistogramOpts{ Name: "alertmanager_oversize_gossip_message_duration_seconds", Help: "Duration of oversized gossip message requests.", ConstLabels: prometheus.Labels{"key": key}, }) reg.MustRegister(oversizeGossipDuration, oversizeGossipMessageFailureTotal, oversizeGossipMessageDroppedTotal, oversizeGossipMessageSentTotal) c := &Channel{ key: key, send: send, peers: peers, logger: logger, msgc: make(chan []byte, 200), sendOversize: sendOversize, oversizeGossipMessageFailureTotal: oversizeGossipMessageFailureTotal, oversizeGossipMessageDroppedTotal: oversizeGossipMessageDroppedTotal, oversizeGossipMessageSentTotal: oversizeGossipMessageSentTotal, oversizeGossipDuration: oversizeGossipDuration, } go c.handleOverSizedMessages(stopc) return c } // handleOverSizedMessages prevents memberlist from opening too many parallel // TCP connections to its peers. func (c *Channel) handleOverSizedMessages(stopc chan struct{}) { var wg sync.WaitGroup for { select { case b := <-c.msgc: for _, n := range c.peers() { wg.Add(1) go func(n *memberlist.Node) { defer wg.Done() c.oversizeGossipMessageSentTotal.Inc() start := time.Now() if err := c.sendOversize(n, b); err != nil { level.Debug(c.logger).Log("msg", "failed to send reliable", "key", c.key, "node", n, "err", err) c.oversizeGossipMessageFailureTotal.Inc() return } c.oversizeGossipDuration.Observe(time.Since(start).Seconds()) }(n) } wg.Wait() case <-stopc: return } } } // Broadcast enqueues a message for broadcasting. func (c *Channel) Broadcast(b []byte) { b, err := proto.Marshal(&clusterpb.Part{Key: c.key, Data: b}) if err != nil { return } if OversizedMessage(b) { select { case c.msgc <- b: default: level.Debug(c.logger).Log("msg", "oversized gossip channel full") c.oversizeGossipMessageDroppedTotal.Inc() } } else { c.send(b) } } // OversizedMessage indicates whether or not the byte payload should be sent // via TCP. func OversizedMessage(b []byte) bool { return len(b) > maxGossipPacketSize/2 } prometheus-alertmanager-0.15.3+ds/cluster/channel_test.go000066400000000000000000000041461341674552200235320ustar00rootroot00000000000000// Copyright 2018 Prometheus Team // 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 cluster import ( "bytes" "context" "io" "os" "testing" "github.com/go-kit/kit/log" "github.com/hashicorp/memberlist" "github.com/prometheus/client_golang/prometheus" ) func TestNormalMessagesGossiped(t *testing.T) { var sent bool c := newChannel( func(_ []byte) { sent = true }, func() []*memberlist.Node { return nil }, func(_ *memberlist.Node, _ []byte) error { return nil }, ) c.Broadcast([]byte{}) if sent != true { t.Fatalf("small message not sent") } } func TestOversizedMessagesGossiped(t *testing.T) { var sent bool ctx, cancel := context.WithCancel(context.Background()) c := newChannel( func(_ []byte) {}, func() []*memberlist.Node { return []*memberlist.Node{&memberlist.Node{}} }, func(_ *memberlist.Node, _ []byte) error { sent = true; cancel(); return nil }, ) f, err := os.Open("/dev/zero") if err != nil { t.Fatalf("failed to open /dev/zero: %v", err) } defer f.Close() buf := new(bytes.Buffer) toCopy := int64(800) if n, err := io.CopyN(buf, f, toCopy); err != nil { t.Fatalf("failed to copy bytes: %v", err) } else if n != toCopy { t.Fatalf("wanted to copy %d bytes, only copied %d", toCopy, n) } c.Broadcast(buf.Bytes()) <-ctx.Done() if sent != true { t.Fatalf("oversized message not sent") } } func newChannel( send func([]byte), peers func() []*memberlist.Node, sendOversize func(*memberlist.Node, []byte) error, ) *Channel { return NewChannel( "test", send, peers, sendOversize, log.NewNopLogger(), make(chan struct{}), prometheus.NewRegistry(), ) } prometheus-alertmanager-0.15.3+ds/cluster/cluster.go000066400000000000000000000463551341674552200225540ustar00rootroot00000000000000// Copyright 2018 Prometheus Team // 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 cluster import ( "context" "fmt" "math/rand" "net" "sort" "strconv" "strings" "sync" "time" "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" "github.com/hashicorp/memberlist" "github.com/oklog/ulid" "github.com/pkg/errors" "github.com/prometheus/client_golang/prometheus" ) // Peer is a single peer in a gossip cluster. type Peer struct { mlist *memberlist.Memberlist delegate *delegate resolvedPeers []string mtx sync.RWMutex states map[string]State stopc chan struct{} readyc chan struct{} peerLock sync.RWMutex peers map[string]peer failedPeers []peer failedReconnectionsCounter prometheus.Counter reconnectionsCounter prometheus.Counter peerLeaveCounter prometheus.Counter peerUpdateCounter prometheus.Counter peerJoinCounter prometheus.Counter logger log.Logger } // peer is an internal type used for bookkeeping. It holds the state of peers // in the cluster. type peer struct { status PeerStatus leaveTime time.Time *memberlist.Node } // PeerStatus is the state that a peer is in. type PeerStatus int const ( StatusNone PeerStatus = iota StatusAlive StatusFailed ) func (s PeerStatus) String() string { switch s { case StatusNone: return "none" case StatusAlive: return "alive" case StatusFailed: return "failed" default: panic(fmt.Sprintf("unknown PeerStatus: %d", s)) } } const ( DefaultPushPullInterval = 60 * time.Second DefaultGossipInterval = 200 * time.Millisecond DefaultTcpTimeout = 10 * time.Second DefaultProbeTimeout = 500 * time.Millisecond DefaultProbeInterval = 1 * time.Second DefaultReconnectInterval = 10 * time.Second DefaultReconnectTimeout = 6 * time.Hour maxGossipPacketSize = 1400 ) func Create( l log.Logger, reg prometheus.Registerer, bindAddr string, advertiseAddr string, knownPeers []string, waitIfEmpty bool, pushPullInterval time.Duration, gossipInterval time.Duration, tcpTimeout time.Duration, probeTimeout time.Duration, probeInterval time.Duration, ) (*Peer, error) { bindHost, bindPortStr, err := net.SplitHostPort(bindAddr) if err != nil { return nil, err } bindPort, err := strconv.Atoi(bindPortStr) if err != nil { return nil, errors.Wrap(err, "invalid listen address") } var advertiseHost string var advertisePort int if advertiseAddr != "" { var advertisePortStr string advertiseHost, advertisePortStr, err = net.SplitHostPort(advertiseAddr) if err != nil { return nil, errors.Wrap(err, "invalid advertise address") } advertisePort, err = strconv.Atoi(advertisePortStr) if err != nil { return nil, errors.Wrap(err, "invalid advertise address, wrong port") } } resolvedPeers, err := resolvePeers(context.Background(), knownPeers, advertiseAddr, net.Resolver{}, waitIfEmpty) if err != nil { return nil, errors.Wrap(err, "resolve peers") } level.Debug(l).Log("msg", "resolved peers to following addresses", "peers", strings.Join(resolvedPeers, ",")) // Initial validation of user-specified advertise address. addr, err := calculateAdvertiseAddress(bindHost, advertiseHost) if err != nil { level.Warn(l).Log("err", "couldn't deduce an advertise address: "+err.Error()) } else if hasNonlocal(resolvedPeers) && isUnroutable(addr.String()) { level.Warn(l).Log("err", "this node advertises itself on an unroutable address", "addr", addr.String()) level.Warn(l).Log("err", "this node will be unreachable in the cluster") level.Warn(l).Log("err", "provide --cluster.advertise-address as a routable IP address or hostname") } else if isAny(bindAddr) && advertiseHost == "" { // memberlist doesn't advertise properly when the bind address is empty or unspecified. level.Info(l).Log("msg", "setting advertise address explicitly", "addr", addr.String(), "port", bindPort) advertiseHost = addr.String() advertisePort = bindPort } // TODO(fabxc): generate human-readable but random names? name, err := ulid.New(ulid.Now(), rand.New(rand.NewSource(time.Now().UnixNano()))) if err != nil { return nil, err } p := &Peer{ states: map[string]State{}, stopc: make(chan struct{}), readyc: make(chan struct{}), logger: l, peers: map[string]peer{}, resolvedPeers: resolvedPeers, } p.register(reg) retransmit := len(knownPeers) / 2 if retransmit < 3 { retransmit = 3 } p.delegate = newDelegate(l, reg, p, retransmit) cfg := memberlist.DefaultLANConfig() cfg.Name = name.String() cfg.BindAddr = bindHost cfg.BindPort = bindPort cfg.Delegate = p.delegate cfg.Events = p.delegate cfg.GossipInterval = gossipInterval cfg.PushPullInterval = pushPullInterval cfg.TCPTimeout = tcpTimeout cfg.ProbeTimeout = probeTimeout cfg.ProbeInterval = probeInterval cfg.LogOutput = &logWriter{l: l} cfg.GossipNodes = retransmit cfg.UDPBufferSize = maxGossipPacketSize if advertiseHost != "" { cfg.AdvertiseAddr = advertiseHost cfg.AdvertisePort = advertisePort p.setInitialFailed(resolvedPeers, fmt.Sprintf("%s:%d", advertiseHost, advertisePort)) } else { p.setInitialFailed(resolvedPeers, bindAddr) } ml, err := memberlist.Create(cfg) if err != nil { return nil, errors.Wrap(err, "create memberlist") } p.mlist = ml return p, nil } func (p *Peer) Join( reconnectInterval time.Duration, reconnectTimeout time.Duration) error { n, err := p.mlist.Join(p.resolvedPeers) if err != nil { level.Warn(p.logger).Log("msg", "failed to join cluster", "err", err) if reconnectInterval != 0 { level.Info(p.logger).Log("msg", fmt.Sprintf("will retry joining cluster every %v", reconnectInterval.String())) } } else { level.Debug(p.logger).Log("msg", "joined cluster", "peers", n) } if reconnectInterval != 0 { go p.handleReconnect(reconnectInterval) } if reconnectTimeout != 0 { go p.handleReconnectTimeout(5*time.Minute, reconnectTimeout) } return err } // All peers are initially added to the failed list. They will be removed from // this list in peerJoin when making their initial connection. func (p *Peer) setInitialFailed(peers []string, myAddr string) { if len(peers) == 0 { return } p.peerLock.RLock() defer p.peerLock.RUnlock() now := time.Now() for _, peerAddr := range peers { if peerAddr == myAddr { // Don't add ourselves to the initially failing list, // we don't connect to ourselves. continue } host, port, err := net.SplitHostPort(peerAddr) if err != nil { continue } ip := net.ParseIP(host) if ip == nil { // Don't add textual addresses since memberlist only advertises // dotted decimal or IPv6 addresses. continue } portUint, err := strconv.ParseUint(port, 10, 16) if err != nil { continue } pr := peer{ status: StatusFailed, leaveTime: now, Node: &memberlist.Node{ Addr: ip, Port: uint16(portUint), }, } p.failedPeers = append(p.failedPeers, pr) p.peers[peerAddr] = pr } } type logWriter struct { l log.Logger } func (l *logWriter) Write(b []byte) (int, error) { return len(b), level.Debug(l.l).Log("memberlist", string(b)) } func (p *Peer) register(reg prometheus.Registerer) { clusterFailedPeers := prometheus.NewGaugeFunc(prometheus.GaugeOpts{ Name: "alertmanager_cluster_failed_peers", Help: "Number indicating the current number of failed peers in the cluster.", }, func() float64 { p.peerLock.RLock() defer p.peerLock.RUnlock() return float64(len(p.failedPeers)) }) p.failedReconnectionsCounter = prometheus.NewCounter(prometheus.CounterOpts{ Name: "alertmanager_cluster_reconnections_failed_total", Help: "A counter of the number of failed cluster peer reconnection attempts.", }) p.reconnectionsCounter = prometheus.NewCounter(prometheus.CounterOpts{ Name: "alertmanager_cluster_reconnections_total", Help: "A counter of the number of cluster peer reconnections.", }) p.peerLeaveCounter = prometheus.NewCounter(prometheus.CounterOpts{ Name: "alertmanager_cluster_peers_left_total", Help: "A counter of the number of peers that have left.", }) p.peerUpdateCounter = prometheus.NewCounter(prometheus.CounterOpts{ Name: "alertmanager_cluster_peers_update_total", Help: "A counter of the number of peers that have updated metadata.", }) p.peerJoinCounter = prometheus.NewCounter(prometheus.CounterOpts{ Name: "alertmanager_cluster_peers_joined_total", Help: "A counter of the number of peers that have joined.", }) reg.MustRegister(clusterFailedPeers, p.failedReconnectionsCounter, p.reconnectionsCounter, p.peerLeaveCounter, p.peerUpdateCounter, p.peerJoinCounter) } func (p *Peer) handleReconnectTimeout(d time.Duration, timeout time.Duration) { tick := time.NewTicker(d) defer tick.Stop() for { select { case <-p.stopc: return case <-tick.C: p.removeFailedPeers(timeout) } } } func (p *Peer) removeFailedPeers(timeout time.Duration) { p.peerLock.Lock() defer p.peerLock.Unlock() now := time.Now() keep := make([]peer, 0, len(p.failedPeers)) for _, pr := range p.failedPeers { if pr.leaveTime.Add(timeout).After(now) { keep = append(keep, pr) } else { level.Debug(p.logger).Log("msg", "failed peer has timed out", "peer", pr.Node, "addr", pr.Address()) delete(p.peers, pr.Name) } } p.failedPeers = keep } func (p *Peer) handleReconnect(d time.Duration) { tick := time.NewTicker(d) defer tick.Stop() for { select { case <-p.stopc: return case <-tick.C: p.reconnect() } } } func (p *Peer) reconnect() { p.peerLock.RLock() failedPeers := p.failedPeers p.peerLock.RUnlock() logger := log.With(p.logger, "msg", "reconnect") for _, pr := range failedPeers { // No need to do book keeping on failedPeers here. If a // reconnect is successful, they will be announced in // peerJoin(). if _, err := p.mlist.Join([]string{pr.Address()}); err != nil { p.failedReconnectionsCounter.Inc() level.Debug(logger).Log("result", "failure", "peer", pr.Node, "addr", pr.Address()) } else { p.reconnectionsCounter.Inc() level.Debug(logger).Log("result", "success", "peer", pr.Node, "addr", pr.Address()) } } } func (p *Peer) peerJoin(n *memberlist.Node) { p.peerLock.Lock() defer p.peerLock.Unlock() var oldStatus PeerStatus pr, ok := p.peers[n.Address()] if !ok { oldStatus = StatusNone pr = peer{ status: StatusAlive, Node: n, } } else { oldStatus = pr.status pr.Node = n pr.status = StatusAlive pr.leaveTime = time.Time{} } p.peers[n.Address()] = pr p.peerJoinCounter.Inc() if oldStatus == StatusFailed { level.Debug(p.logger).Log("msg", "peer rejoined", "peer", pr.Node) p.failedPeers = removeOldPeer(p.failedPeers, pr.Address()) } } func (p *Peer) peerLeave(n *memberlist.Node) { p.peerLock.Lock() defer p.peerLock.Unlock() pr, ok := p.peers[n.Address()] if !ok { // Why are we receiving a leave notification from a node that // never joined? return } pr.status = StatusFailed pr.leaveTime = time.Now() p.failedPeers = append(p.failedPeers, pr) p.peers[n.Address()] = pr p.peerLeaveCounter.Inc() level.Debug(p.logger).Log("msg", "peer left", "peer", pr.Node) } func (p *Peer) peerUpdate(n *memberlist.Node) { p.peerLock.Lock() defer p.peerLock.Unlock() pr, ok := p.peers[n.Address()] if !ok { // Why are we receiving an update from a node that never // joined? return } pr.Node = n p.peers[n.Address()] = pr p.peerUpdateCounter.Inc() level.Debug(p.logger).Log("msg", "peer updated", "peer", pr.Node) } // AddState adds a new state that will be gossiped. It returns a channel to which // broadcast messages for the state can be sent. func (p *Peer) AddState(key string, s State, reg prometheus.Registerer) *Channel { p.states[key] = s send := func(b []byte) { p.delegate.bcast.QueueBroadcast(simpleBroadcast(b)) } peers := func() []*memberlist.Node { nodes := p.Peers() for i, n := range nodes { if n.Name == p.Self().Name { nodes = append(nodes[:i], nodes[i+1:]...) break } } return nodes } sendOversize := func(n *memberlist.Node, b []byte) error { return p.mlist.SendReliable(n, b) } return NewChannel(key, send, peers, sendOversize, p.logger, p.stopc, reg) } // Leave the cluster, waiting up to timeout. func (p *Peer) Leave(timeout time.Duration) error { close(p.stopc) level.Debug(p.logger).Log("msg", "leaving cluster") return p.mlist.Leave(timeout) } // Name returns the unique ID of this peer in the cluster. func (p *Peer) Name() string { return p.mlist.LocalNode().Name } // ClusterSize returns the current number of alive members in the cluster. func (p *Peer) ClusterSize() int { return p.mlist.NumMembers() } // Return true when router has settled. func (p *Peer) Ready() bool { select { case <-p.readyc: return true default: } return false } // Wait until Settle() has finished. func (p *Peer) WaitReady() { <-p.readyc } // Return a status string representing the peer state. func (p *Peer) Status() string { if p.Ready() { return "ready" } else { return "settling" } } // Info returns a JSON-serializable dump of cluster state. // Useful for debug. func (p *Peer) Info() map[string]interface{} { p.mtx.RLock() defer p.mtx.RUnlock() return map[string]interface{}{ "self": p.mlist.LocalNode(), "members": p.mlist.Members(), } } // Self returns the node information about the peer itself. func (p *Peer) Self() *memberlist.Node { return p.mlist.LocalNode() } // Peers returns the peers in the cluster. func (p *Peer) Peers() []*memberlist.Node { return p.mlist.Members() } // Position returns the position of the peer in the cluster. func (p *Peer) Position() int { all := p.Peers() sort.Slice(all, func(i, j int) bool { return all[i].Name < all[j].Name }) k := 0 for _, n := range all { if n.Name == p.Self().Name { break } k++ } return k } // Settle waits until the mesh is ready (and sets the appropriate internal state when it is). // The idea is that we don't want to start "working" before we get a chance to know most of the alerts and/or silences. // Inspired from https://github.com/apache/cassandra/blob/7a40abb6a5108688fb1b10c375bb751cbb782ea4/src/java/org/apache/cassandra/gms/Gossiper.java // This is clearly not perfect or strictly correct but should prevent the alertmanager to send notification before it is obviously not ready. // This is especially important for those that do not have persistent storage. func (p *Peer) Settle(ctx context.Context, interval time.Duration) { const NumOkayRequired = 3 level.Info(p.logger).Log("msg", "Waiting for gossip to settle...", "interval", interval) start := time.Now() nPeers := 0 nOkay := 0 totalPolls := 0 for { select { case <-ctx.Done(): elapsed := time.Since(start) level.Info(p.logger).Log("msg", "gossip not settled but continuing anyway", "polls", totalPolls, "elapsed", elapsed) close(p.readyc) return case <-time.After(interval): } elapsed := time.Since(start) n := len(p.Peers()) if nOkay >= NumOkayRequired { level.Info(p.logger).Log("msg", "gossip settled; proceeding", "elapsed", elapsed) break } if n == nPeers { nOkay++ level.Debug(p.logger).Log("msg", "gossip looks settled", "elapsed", elapsed) } else { nOkay = 0 level.Info(p.logger).Log("msg", "gossip not settled", "polls", totalPolls, "before", nPeers, "now", n, "elapsed", elapsed) } nPeers = n totalPolls++ } close(p.readyc) } // State is a piece of state that can be serialized and merged with other // serialized state. type State interface { // MarshalBinary serializes the underlying state. MarshalBinary() ([]byte, error) // Merge merges serialized state into the underlying state. Merge(b []byte) error } // We use a simple broadcast implementation in which items are never invalidated by others. type simpleBroadcast []byte func (b simpleBroadcast) Message() []byte { return []byte(b) } func (b simpleBroadcast) Invalidates(memberlist.Broadcast) bool { return false } func (b simpleBroadcast) Finished() {} func resolvePeers(ctx context.Context, peers []string, myAddress string, res net.Resolver, waitIfEmpty bool) ([]string, error) { var resolvedPeers []string for _, peer := range peers { host, port, err := net.SplitHostPort(peer) if err != nil { return nil, errors.Wrapf(err, "split host/port for peer %s", peer) } retryCtx, cancel := context.WithCancel(ctx) ips, err := res.LookupIPAddr(ctx, host) if err != nil { // Assume direct address. resolvedPeers = append(resolvedPeers, peer) continue } if len(ips) == 0 { var lookupErrSpotted bool err := retry(2*time.Second, retryCtx.Done(), func() error { if lookupErrSpotted { // We need to invoke cancel in next run of retry when lookupErrSpotted to preserve LookupIPAddr error. cancel() } ips, err = res.LookupIPAddr(retryCtx, host) if err != nil { lookupErrSpotted = true return errors.Wrapf(err, "IP Addr lookup for peer %s", peer) } ips = removeMyAddr(ips, port, myAddress) if len(ips) == 0 { if !waitIfEmpty { return nil } return errors.New("empty IPAddr result. Retrying") } return nil }) if err != nil { return nil, err } } for _, ip := range ips { resolvedPeers = append(resolvedPeers, net.JoinHostPort(ip.String(), port)) } } return resolvedPeers, nil } func removeMyAddr(ips []net.IPAddr, targetPort string, myAddr string) []net.IPAddr { var result []net.IPAddr for _, ip := range ips { if net.JoinHostPort(ip.String(), targetPort) == myAddr { continue } result = append(result, ip) } return result } func hasNonlocal(clusterPeers []string) bool { for _, peer := range clusterPeers { if host, _, err := net.SplitHostPort(peer); err == nil { peer = host } if ip := net.ParseIP(peer); ip != nil && !ip.IsLoopback() { return true } else if ip == nil && strings.ToLower(peer) != "localhost" { return true } } return false } func isUnroutable(addr string) bool { if host, _, err := net.SplitHostPort(addr); err == nil { addr = host } if ip := net.ParseIP(addr); ip != nil && (ip.IsUnspecified() || ip.IsLoopback()) { return true // typically 0.0.0.0 or localhost } else if ip == nil && strings.ToLower(addr) == "localhost" { return true } return false } func isAny(addr string) bool { if host, _, err := net.SplitHostPort(addr); err == nil { addr = host } return addr == "" || net.ParseIP(addr).IsUnspecified() } // retry executes f every interval seconds until timeout or no error is returned from f. func retry(interval time.Duration, stopc <-chan struct{}, f func() error) error { tick := time.NewTicker(interval) defer tick.Stop() var err error for { if err = f(); err == nil { return nil } select { case <-stopc: return err case <-tick.C: } } } func removeOldPeer(old []peer, addr string) []peer { new := make([]peer, 0, len(old)) for _, p := range old { if p.Address() != addr { new = append(new, p) } } return new } prometheus-alertmanager-0.15.3+ds/cluster/cluster_test.go000066400000000000000000000124321341674552200236000ustar00rootroot00000000000000// Copyright 2018 Prometheus Team // 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 cluster import ( "context" "testing" "time" "github.com/go-kit/kit/log" "github.com/stretchr/testify/require" "github.com/prometheus/client_golang/prometheus" ) func TestJoinLeave(t *testing.T) { logger := log.NewNopLogger() p, err := Create( logger, prometheus.NewRegistry(), "0.0.0.0:0", "", []string{}, true, DefaultPushPullInterval, DefaultGossipInterval, DefaultTcpTimeout, DefaultProbeTimeout, DefaultProbeInterval, ) require.NoError(t, err) require.NotNil(t, p) err = p.Join( DefaultReconnectInterval, DefaultReconnectTimeout, ) require.NoError(t, err) require.False(t, p.Ready()) require.Equal(t, p.Status(), "settling") go p.Settle(context.Background(), 0*time.Second) p.WaitReady() require.Equal(t, p.Status(), "ready") // Create the peer who joins the first. p2, err := Create( logger, prometheus.NewRegistry(), "0.0.0.0:0", "", []string{p.Self().Address()}, true, DefaultPushPullInterval, DefaultGossipInterval, DefaultTcpTimeout, DefaultProbeTimeout, DefaultProbeInterval, ) require.NoError(t, err) require.NotNil(t, p2) err = p2.Join( DefaultReconnectInterval, DefaultReconnectTimeout, ) require.NoError(t, err) go p2.Settle(context.Background(), 0*time.Second) require.Equal(t, 2, p.ClusterSize()) p2.Leave(0 * time.Second) require.Equal(t, 1, p.ClusterSize()) require.Equal(t, 1, len(p.failedPeers)) require.Equal(t, p2.Self().Address(), p.peers[p2.Self().Address()].Node.Address()) require.Equal(t, p2.Name(), p.failedPeers[0].Name) } func TestReconnect(t *testing.T) { logger := log.NewNopLogger() p, err := Create( logger, prometheus.NewRegistry(), "0.0.0.0:0", "", []string{}, true, DefaultPushPullInterval, DefaultGossipInterval, DefaultTcpTimeout, DefaultProbeTimeout, DefaultProbeInterval, ) require.NoError(t, err) require.NotNil(t, p) err = p.Join( DefaultReconnectInterval, DefaultReconnectTimeout, ) require.NoError(t, err) go p.Settle(context.Background(), 0*time.Second) p.WaitReady() p2, err := Create( logger, prometheus.NewRegistry(), "0.0.0.0:0", "", []string{}, true, DefaultPushPullInterval, DefaultGossipInterval, DefaultTcpTimeout, DefaultProbeTimeout, DefaultProbeInterval, ) require.NoError(t, err) require.NotNil(t, p2) err = p2.Join( DefaultReconnectInterval, DefaultReconnectTimeout, ) require.NoError(t, err) go p2.Settle(context.Background(), 0*time.Second) p2.WaitReady() p.peerJoin(p2.Self()) p.peerLeave(p2.Self()) require.Equal(t, 1, p.ClusterSize()) require.Equal(t, 1, len(p.failedPeers)) p.reconnect() require.Equal(t, 2, p.ClusterSize()) require.Equal(t, 0, len(p.failedPeers)) require.Equal(t, StatusAlive, p.peers[p2.Self().Address()].status) } func TestRemoveFailedPeers(t *testing.T) { logger := log.NewNopLogger() p, err := Create( logger, prometheus.NewRegistry(), "0.0.0.0:0", "", []string{}, true, DefaultPushPullInterval, DefaultGossipInterval, DefaultTcpTimeout, DefaultProbeTimeout, DefaultProbeInterval, ) require.NoError(t, err) require.NotNil(t, p) err = p.Join( DefaultReconnectInterval, DefaultReconnectTimeout, ) require.NoError(t, err) n := p.Self() now := time.Now() p1 := peer{ status: StatusFailed, leaveTime: now, Node: n, } p2 := peer{ status: StatusFailed, leaveTime: now.Add(-time.Hour), Node: n, } p3 := peer{ status: StatusFailed, leaveTime: now.Add(30 * -time.Minute), Node: n, } p.failedPeers = []peer{p1, p2, p3} p.removeFailedPeers(30 * time.Minute) require.Equal(t, 1, len(p.failedPeers)) require.Equal(t, p1, p.failedPeers[0]) } func TestInitiallyFailingPeers(t *testing.T) { logger := log.NewNopLogger() myAddr := "1.2.3.4:5000" peerAddrs := []string{myAddr, "2.3.4.5:5000", "3.4.5.6:5000", "foo.example.com:5000"} p, err := Create( logger, prometheus.NewRegistry(), "0.0.0.0:0", "", []string{}, true, DefaultPushPullInterval, DefaultGossipInterval, DefaultTcpTimeout, DefaultProbeTimeout, DefaultProbeInterval, ) require.NoError(t, err) require.NotNil(t, p) err = p.Join( DefaultReconnectInterval, DefaultReconnectTimeout, ) require.NoError(t, err) p.setInitialFailed(peerAddrs, myAddr) // We shouldn't have added "our" bind addr and the FQDN address to the // failed peers list. require.Equal(t, len(peerAddrs)-2, len(p.failedPeers)) for _, addr := range peerAddrs { if addr == myAddr || addr == "foo.example.com:5000" { continue } pr, ok := p.peers[addr] require.True(t, ok) require.Equal(t, StatusFailed.String(), pr.status.String()) require.Equal(t, addr, pr.Address()) expectedLen := len(p.failedPeers) - 1 p.peerJoin(pr.Node) require.Equal(t, expectedLen, len(p.failedPeers)) } } prometheus-alertmanager-0.15.3+ds/cluster/clusterpb/000077500000000000000000000000001341674552200225325ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/cluster/clusterpb/cluster.pb.go000066400000000000000000000254331341674552200251510ustar00rootroot00000000000000// Code generated by protoc-gen-gogo. DO NOT EDIT. // source: cluster.proto /* Package clusterpb is a generated protocol buffer package. It is generated from these files: cluster.proto It has these top-level messages: Part FullState */ package clusterpb import proto "github.com/gogo/protobuf/proto" import fmt "fmt" import math "math" import _ "github.com/gogo/protobuf/gogoproto" import io "io" // Reference imports to suppress errors if they are not otherwise used. var _ = proto.Marshal var _ = fmt.Errorf var _ = math.Inf // This is a compile-time assertion to ensure that this generated file // is compatible with the proto package it is being compiled against. // A compilation error at this line likely means your copy of the // proto package needs to be updated. const _ = proto.GoGoProtoPackageIsVersion2 // please upgrade the proto package type Part struct { Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` } func (m *Part) Reset() { *m = Part{} } func (m *Part) String() string { return proto.CompactTextString(m) } func (*Part) ProtoMessage() {} func (*Part) Descriptor() ([]byte, []int) { return fileDescriptorCluster, []int{0} } type FullState struct { Parts []Part `protobuf:"bytes,1,rep,name=parts" json:"parts"` } func (m *FullState) Reset() { *m = FullState{} } func (m *FullState) String() string { return proto.CompactTextString(m) } func (*FullState) ProtoMessage() {} func (*FullState) Descriptor() ([]byte, []int) { return fileDescriptorCluster, []int{1} } func init() { proto.RegisterType((*Part)(nil), "clusterpb.Part") proto.RegisterType((*FullState)(nil), "clusterpb.FullState") } func (m *Part) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalTo(dAtA) if err != nil { return nil, err } return dAtA[:n], nil } func (m *Part) MarshalTo(dAtA []byte) (int, error) { var i int _ = i var l int _ = l if len(m.Key) > 0 { dAtA[i] = 0xa i++ i = encodeVarintCluster(dAtA, i, uint64(len(m.Key))) i += copy(dAtA[i:], m.Key) } if len(m.Data) > 0 { dAtA[i] = 0x12 i++ i = encodeVarintCluster(dAtA, i, uint64(len(m.Data))) i += copy(dAtA[i:], m.Data) } return i, nil } func (m *FullState) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalTo(dAtA) if err != nil { return nil, err } return dAtA[:n], nil } func (m *FullState) MarshalTo(dAtA []byte) (int, error) { var i int _ = i var l int _ = l if len(m.Parts) > 0 { for _, msg := range m.Parts { dAtA[i] = 0xa i++ i = encodeVarintCluster(dAtA, i, uint64(msg.Size())) n, err := msg.MarshalTo(dAtA[i:]) if err != nil { return 0, err } i += n } } return i, nil } func encodeVarintCluster(dAtA []byte, offset int, v uint64) int { for v >= 1<<7 { dAtA[offset] = uint8(v&0x7f | 0x80) v >>= 7 offset++ } dAtA[offset] = uint8(v) return offset + 1 } func (m *Part) Size() (n int) { var l int _ = l l = len(m.Key) if l > 0 { n += 1 + l + sovCluster(uint64(l)) } l = len(m.Data) if l > 0 { n += 1 + l + sovCluster(uint64(l)) } return n } func (m *FullState) Size() (n int) { var l int _ = l if len(m.Parts) > 0 { for _, e := range m.Parts { l = e.Size() n += 1 + l + sovCluster(uint64(l)) } } return n } func sovCluster(x uint64) (n int) { for { n++ x >>= 7 if x == 0 { break } } return n } func sozCluster(x uint64) (n int) { return sovCluster(uint64((x << 1) ^ uint64((int64(x) >> 63)))) } func (m *Part) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowCluster } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: Part: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: Part: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Key", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowCluster } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ stringLen |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } intStringLen := int(stringLen) if intStringLen < 0 { return ErrInvalidLengthCluster } postIndex := iNdEx + intStringLen if postIndex > l { return io.ErrUnexpectedEOF } m.Key = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex case 2: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Data", wireType) } var byteLen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowCluster } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ byteLen |= (int(b) & 0x7F) << shift if b < 0x80 { break } } if byteLen < 0 { return ErrInvalidLengthCluster } postIndex := iNdEx + byteLen if postIndex > l { return io.ErrUnexpectedEOF } m.Data = append(m.Data[:0], dAtA[iNdEx:postIndex]...) if m.Data == nil { m.Data = []byte{} } iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipCluster(dAtA[iNdEx:]) if err != nil { return err } if skippy < 0 { return ErrInvalidLengthCluster } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *FullState) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowCluster } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: FullState: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: FullState: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Parts", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowCluster } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= (int(b) & 0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthCluster } postIndex := iNdEx + msglen if postIndex > l { return io.ErrUnexpectedEOF } m.Parts = append(m.Parts, Part{}) if err := m.Parts[len(m.Parts)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipCluster(dAtA[iNdEx:]) if err != nil { return err } if skippy < 0 { return ErrInvalidLengthCluster } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func skipCluster(dAtA []byte) (n int, err error) { l := len(dAtA) iNdEx := 0 for iNdEx < l { var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return 0, ErrIntOverflowCluster } if iNdEx >= l { return 0, io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } wireType := int(wire & 0x7) switch wireType { case 0: for shift := uint(0); ; shift += 7 { if shift >= 64 { return 0, ErrIntOverflowCluster } if iNdEx >= l { return 0, io.ErrUnexpectedEOF } iNdEx++ if dAtA[iNdEx-1] < 0x80 { break } } return iNdEx, nil case 1: iNdEx += 8 return iNdEx, nil case 2: var length int for shift := uint(0); ; shift += 7 { if shift >= 64 { return 0, ErrIntOverflowCluster } if iNdEx >= l { return 0, io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ length |= (int(b) & 0x7F) << shift if b < 0x80 { break } } iNdEx += length if length < 0 { return 0, ErrInvalidLengthCluster } return iNdEx, nil case 3: for { var innerWire uint64 var start int = iNdEx for shift := uint(0); ; shift += 7 { if shift >= 64 { return 0, ErrIntOverflowCluster } if iNdEx >= l { return 0, io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ innerWire |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } innerWireType := int(innerWire & 0x7) if innerWireType == 4 { break } next, err := skipCluster(dAtA[start:]) if err != nil { return 0, err } iNdEx = start + next } return iNdEx, nil case 4: return iNdEx, nil case 5: iNdEx += 4 return iNdEx, nil default: return 0, fmt.Errorf("proto: illegal wireType %d", wireType) } } panic("unreachable") } var ( ErrInvalidLengthCluster = fmt.Errorf("proto: negative length found during unmarshaling") ErrIntOverflowCluster = fmt.Errorf("proto: integer overflow") ) func init() { proto.RegisterFile("cluster.proto", fileDescriptorCluster) } var fileDescriptorCluster = []byte{ // 168 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x4d, 0xce, 0x29, 0x2d, 0x2e, 0x49, 0x2d, 0xd2, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x84, 0x72, 0x0b, 0x92, 0xa4, 0x44, 0xd2, 0xf3, 0xd3, 0xf3, 0xc1, 0xa2, 0xfa, 0x20, 0x16, 0x44, 0x81, 0x92, 0x0e, 0x17, 0x4b, 0x40, 0x62, 0x51, 0x89, 0x90, 0x00, 0x17, 0x73, 0x76, 0x6a, 0xa5, 0x04, 0xa3, 0x02, 0xa3, 0x06, 0x67, 0x10, 0x88, 0x29, 0x24, 0xc4, 0xc5, 0x92, 0x92, 0x58, 0x92, 0x28, 0xc1, 0xa4, 0xc0, 0xa8, 0xc1, 0x13, 0x04, 0x66, 0x2b, 0x59, 0x70, 0x71, 0xba, 0x95, 0xe6, 0xe4, 0x04, 0x97, 0x24, 0x96, 0xa4, 0x0a, 0x69, 0x73, 0xb1, 0x16, 0x24, 0x16, 0x95, 0x14, 0x4b, 0x30, 0x2a, 0x30, 0x6b, 0x70, 0x1b, 0xf1, 0xeb, 0xc1, 0xed, 0xd2, 0x03, 0x19, 0xe9, 0xc4, 0x72, 0xe2, 0x9e, 0x3c, 0x43, 0x10, 0x44, 0x8d, 0x93, 0xc0, 0x89, 0x87, 0x72, 0x0c, 0x27, 0x1e, 0xc9, 0x31, 0x5e, 0x78, 0x24, 0xc7, 0xf8, 0xe0, 0x91, 0x1c, 0x63, 0x12, 0x1b, 0xd8, 0x01, 0xc6, 0x80, 0x00, 0x00, 0x00, 0xff, 0xff, 0xfd, 0x3c, 0xdb, 0xe7, 0xb2, 0x00, 0x00, 0x00, } prometheus-alertmanager-0.15.3+ds/cluster/clusterpb/cluster.proto000066400000000000000000000005711341674552200253030ustar00rootroot00000000000000syntax = "proto3"; package clusterpb; import "gogoproto/gogo.proto"; option (gogoproto.marshaler_all) = true; option (gogoproto.sizer_all) = true; option (gogoproto.unmarshaler_all) = true; option (gogoproto.goproto_getters_all) = false; message Part { string key = 1; bytes data = 2; } message FullState { repeated Part parts = 1 [(gogoproto.nullable) = false]; } prometheus-alertmanager-0.15.3+ds/cluster/delegate.go000066400000000000000000000200071341674552200226270ustar00rootroot00000000000000// Copyright 2018 Prometheus Team // 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 cluster import ( "time" "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" "github.com/gogo/protobuf/proto" "github.com/hashicorp/memberlist" "github.com/prometheus/alertmanager/cluster/clusterpb" "github.com/prometheus/client_golang/prometheus" ) // Maximum number of messages to be held in the queue. const maxQueueSize = 4096 // delegate implements memberlist.Delegate and memberlist.EventDelegate // and broadcasts its peer's state in the cluster. type delegate struct { *Peer logger log.Logger bcast *memberlist.TransmitLimitedQueue messagesReceived *prometheus.CounterVec messagesReceivedSize *prometheus.CounterVec messagesSent *prometheus.CounterVec messagesSentSize *prometheus.CounterVec messagesPruned prometheus.Counter } func newDelegate(l log.Logger, reg prometheus.Registerer, p *Peer, retransmit int) *delegate { bcast := &memberlist.TransmitLimitedQueue{ NumNodes: p.ClusterSize, RetransmitMult: retransmit, } messagesReceived := prometheus.NewCounterVec(prometheus.CounterOpts{ Name: "alertmanager_cluster_messages_received_total", Help: "Total number of cluster messsages received.", }, []string{"msg_type"}) messagesReceivedSize := prometheus.NewCounterVec(prometheus.CounterOpts{ Name: "alertmanager_cluster_messages_received_size_total", Help: "Total size of cluster messages received.", }, []string{"msg_type"}) messagesSent := prometheus.NewCounterVec(prometheus.CounterOpts{ Name: "alertmanager_cluster_messages_sent_total", Help: "Total number of cluster messsages sent.", }, []string{"msg_type"}) messagesSentSize := prometheus.NewCounterVec(prometheus.CounterOpts{ Name: "alertmanager_cluster_messages_sent_size_total", Help: "Total size of cluster messages sent.", }, []string{"msg_type"}) messagesPruned := prometheus.NewCounter(prometheus.CounterOpts{ Name: "alertmanager_cluster_messages_pruned_total", Help: "Total number of cluster messsages pruned.", }) gossipClusterMembers := prometheus.NewGaugeFunc(prometheus.GaugeOpts{ Name: "alertmanager_cluster_members", Help: "Number indicating current number of members in cluster.", }, func() float64 { return float64(p.ClusterSize()) }) peerPosition := prometheus.NewGaugeFunc(prometheus.GaugeOpts{ Name: "alertmanager_peer_position", Help: "Position the Alertmanager instance believes it's in. The position determines a peer's behavior in the cluster.", }, func() float64 { return float64(p.Position()) }) healthScore := prometheus.NewGaugeFunc(prometheus.GaugeOpts{ Name: "alertmanager_cluster_health_score", Help: "Health score of the cluster. Lower values are better and zero means 'totally healthy'.", }, func() float64 { return float64(p.mlist.GetHealthScore()) }) messagesQueued := prometheus.NewGaugeFunc(prometheus.GaugeOpts{ Name: "alertmanager_cluster_messages_queued", Help: "Number of cluster messsages which are queued.", }, func() float64 { return float64(bcast.NumQueued()) }) messagesReceived.WithLabelValues("full_state") messagesReceivedSize.WithLabelValues("full_state") messagesReceived.WithLabelValues("update") messagesReceivedSize.WithLabelValues("update") messagesSent.WithLabelValues("full_state") messagesSentSize.WithLabelValues("full_state") messagesSent.WithLabelValues("update") messagesSentSize.WithLabelValues("update") reg.MustRegister(messagesReceived, messagesReceivedSize, messagesSent, messagesSentSize, gossipClusterMembers, peerPosition, healthScore, messagesQueued, messagesPruned) d := &delegate{ logger: l, Peer: p, bcast: bcast, messagesReceived: messagesReceived, messagesReceivedSize: messagesReceivedSize, messagesSent: messagesSent, messagesSentSize: messagesSentSize, messagesPruned: messagesPruned, } go d.handleQueueDepth() return d } // NodeMeta retrieves meta-data about the current node when broadcasting an alive message. func (d *delegate) NodeMeta(limit int) []byte { return []byte{} } // NotifyMsg is the callback invoked when a user-level gossip message is received. func (d *delegate) NotifyMsg(b []byte) { d.messagesReceived.WithLabelValues("update").Inc() d.messagesReceivedSize.WithLabelValues("update").Add(float64(len(b))) var p clusterpb.Part if err := proto.Unmarshal(b, &p); err != nil { level.Warn(d.logger).Log("msg", "decode broadcast", "err", err) return } s, ok := d.states[p.Key] if !ok { return } if err := s.Merge(p.Data); err != nil { level.Warn(d.logger).Log("msg", "merge broadcast", "err", err, "key", p.Key) return } } // GetBroadcasts is called when user data messages can be broadcasted. func (d *delegate) GetBroadcasts(overhead, limit int) [][]byte { msgs := d.bcast.GetBroadcasts(overhead, limit) d.messagesSent.WithLabelValues("update").Add(float64(len(msgs))) for _, m := range msgs { d.messagesSentSize.WithLabelValues("update").Add(float64(len(m))) } return msgs } // LocalState is called when gossip fetches local state. func (d *delegate) LocalState(_ bool) []byte { all := &clusterpb.FullState{ Parts: make([]clusterpb.Part, 0, len(d.states)), } for key, s := range d.states { b, err := s.MarshalBinary() if err != nil { level.Warn(d.logger).Log("msg", "encode local state", "err", err, "key", key) return nil } all.Parts = append(all.Parts, clusterpb.Part{Key: key, Data: b}) } b, err := proto.Marshal(all) if err != nil { level.Warn(d.logger).Log("msg", "encode local state", "err", err) return nil } d.messagesSent.WithLabelValues("full_state").Inc() d.messagesSentSize.WithLabelValues("full_state").Add(float64(len(b))) return b } func (d *delegate) MergeRemoteState(buf []byte, _ bool) { d.messagesReceived.WithLabelValues("full_state").Inc() d.messagesReceivedSize.WithLabelValues("full_state").Add(float64(len(buf))) var fs clusterpb.FullState if err := proto.Unmarshal(buf, &fs); err != nil { level.Warn(d.logger).Log("msg", "merge remote state", "err", err) return } d.mtx.RLock() defer d.mtx.RUnlock() for _, p := range fs.Parts { s, ok := d.states[p.Key] if !ok { level.Warn(d.logger).Log("received", "unknown state key", "len", len(buf), "key", p.Key) continue } if err := s.Merge(p.Data); err != nil { level.Warn(d.logger).Log("msg", "merge remote state", "err", err, "key", p.Key) return } } } // NotifyJoin is called if a peer joins the cluster. func (d *delegate) NotifyJoin(n *memberlist.Node) { level.Debug(d.logger).Log("received", "NotifyJoin", "node", n.Name, "addr", n.Address()) d.Peer.peerJoin(n) } // NotifyLeave is called if a peer leaves the cluster. func (d *delegate) NotifyLeave(n *memberlist.Node) { level.Debug(d.logger).Log("received", "NotifyLeave", "node", n.Name, "addr", n.Address()) d.Peer.peerLeave(n) } // NotifyUpdate is called if a cluster peer gets updated. func (d *delegate) NotifyUpdate(n *memberlist.Node) { level.Debug(d.logger).Log("received", "NotifyUpdate", "node", n.Name, "addr", n.Address()) d.Peer.peerUpdate(n) } // handleQueueDepth ensures that the queue doesn't grow unbounded by pruning // older messages at regular interval. func (d *delegate) handleQueueDepth() { for { select { case <-d.stopc: return case <-time.After(15 * time.Minute): n := d.bcast.NumQueued() if n > maxQueueSize { level.Warn(d.logger).Log("msg", "dropping messages because too many are queued", "current", n, "limit", maxQueueSize) d.bcast.Prune(maxQueueSize) d.messagesPruned.Add(float64(n - maxQueueSize)) } } } } prometheus-alertmanager-0.15.3+ds/cmd/000077500000000000000000000000001341674552200176115ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/cmd/alertmanager/000077500000000000000000000000001341674552200222535ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/cmd/alertmanager/main.go000066400000000000000000000366011341674552200235340ustar00rootroot00000000000000// Copyright 2015 Prometheus Team // 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" "crypto/md5" "encoding/binary" "fmt" "net" "net/http" "net/url" "os" "os/signal" "path/filepath" "runtime" "strings" "sync" "syscall" "time" "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" "github.com/prometheus/alertmanager/api" "github.com/prometheus/alertmanager/cluster" "github.com/prometheus/alertmanager/config" "github.com/prometheus/alertmanager/dispatch" "github.com/prometheus/alertmanager/inhibit" "github.com/prometheus/alertmanager/nflog" "github.com/prometheus/alertmanager/notify" "github.com/prometheus/alertmanager/provider/mem" "github.com/prometheus/alertmanager/silence" "github.com/prometheus/alertmanager/template" "github.com/prometheus/alertmanager/types" "github.com/prometheus/alertmanager/ui" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/common/promlog" "github.com/prometheus/common/route" "github.com/prometheus/common/version" "github.com/prometheus/prometheus/pkg/labels" "gopkg.in/alecthomas/kingpin.v2" ) var ( configHash = prometheus.NewGauge(prometheus.GaugeOpts{ Name: "alertmanager_config_hash", Help: "Hash of the currently loaded alertmanager configuration.", }) configSuccess = prometheus.NewGauge(prometheus.GaugeOpts{ Name: "alertmanager_config_last_reload_successful", Help: "Whether the last configuration reload attempt was successful.", }) configSuccessTime = prometheus.NewGauge(prometheus.GaugeOpts{ Name: "alertmanager_config_last_reload_success_timestamp_seconds", Help: "Timestamp of the last successful configuration reload.", }) alertsActive prometheus.GaugeFunc alertsSuppressed prometheus.GaugeFunc requestDuration = prometheus.NewHistogramVec( prometheus.HistogramOpts{ Name: "alertmanager_http_request_duration_seconds", Help: "Histogram of latencies for HTTP requests.", Buckets: []float64{.05, 0.1, .25, .5, .75, 1, 2, 5, 20, 60}, }, []string{"handler", "method"}, ) responseSize = prometheus.NewHistogramVec( prometheus.HistogramOpts{ Name: "alertmanager_http_response_size_bytes", Help: "Histogram of response size for HTTP requests.", Buckets: prometheus.ExponentialBuckets(100, 10, 7), }, []string{"handler", "method"}, ) ) func init() { prometheus.MustRegister(configSuccess) prometheus.MustRegister(configSuccessTime) prometheus.MustRegister(configHash) prometheus.MustRegister(requestDuration) prometheus.MustRegister(responseSize) prometheus.MustRegister(version.NewCollector("alertmanager")) } func instrumentHandler(handlerName string, handler http.HandlerFunc) http.HandlerFunc { return promhttp.InstrumentHandlerDuration( requestDuration.MustCurryWith(prometheus.Labels{"handler": handlerName}), promhttp.InstrumentHandlerResponseSize( responseSize.MustCurryWith(prometheus.Labels{"handler": handlerName}), handler, ), ) } func newAlertMetricByState(marker types.Marker, st types.AlertState) prometheus.GaugeFunc { return prometheus.NewGaugeFunc( prometheus.GaugeOpts{ Name: "alertmanager_alerts", Help: "How many alerts by state.", ConstLabels: prometheus.Labels{"state": string(st)}, }, func() float64 { return float64(marker.Count(st)) }, ) } func newMarkerMetrics(marker types.Marker) { alertsActive = newAlertMetricByState(marker, types.AlertStateActive) alertsSuppressed = newAlertMetricByState(marker, types.AlertStateSuppressed) prometheus.MustRegister(alertsActive) prometheus.MustRegister(alertsSuppressed) } const defaultClusterAddr = "0.0.0.0:9094" func main() { if os.Getenv("DEBUG") != "" { runtime.SetBlockProfileRate(20) runtime.SetMutexProfileFraction(20) } logLevel := &promlog.AllowedLevel{} if err := logLevel.Set("info"); err != nil { panic(err) } var ( configFile = kingpin.Flag("config.file", "Alertmanager configuration file name.").Default("alertmanager.yml").String() dataDir = kingpin.Flag("storage.path", "Base path for data storage.").Default("data/").String() retention = kingpin.Flag("data.retention", "How long to keep data for.").Default("120h").Duration() alertGCInterval = kingpin.Flag("alerts.gc-interval", "Interval between alert GC.").Default("30m").Duration() logLevelString = kingpin.Flag("log.level", "Only log messages with the given severity or above.").Default("info").Enum("debug", "info", "warn", "error") externalURL = kingpin.Flag("web.external-url", "The URL under which Alertmanager is externally reachable (for example, if Alertmanager is served via a reverse proxy). Used for generating relative and absolute links back to Alertmanager itself. If the URL has a path portion, it will be used to prefix all HTTP endpoints served by Alertmanager. If omitted, relevant URL components will be derived automatically.").String() routePrefix = kingpin.Flag("web.route-prefix", "Prefix for the internal routes of web endpoints. Defaults to path of --web.external-url.").String() listenAddress = kingpin.Flag("web.listen-address", "Address to listen on for the web interface and API.").Default(":9093").String() clusterBindAddr = kingpin.Flag("cluster.listen-address", "Listen address for cluster."). Default(defaultClusterAddr).String() clusterAdvertiseAddr = kingpin.Flag("cluster.advertise-address", "Explicit address to advertise in cluster.").String() peers = kingpin.Flag("cluster.peer", "Initial peers (may be repeated).").Strings() peerTimeout = kingpin.Flag("cluster.peer-timeout", "Time to wait between peers to send notifications.").Default("15s").Duration() gossipInterval = kingpin.Flag("cluster.gossip-interval", "Interval between sending gossip messages. By lowering this value (more frequent) gossip messages are propagated across the cluster more quickly at the expense of increased bandwidth.").Default(cluster.DefaultGossipInterval.String()).Duration() pushPullInterval = kingpin.Flag("cluster.pushpull-interval", "Interval for gossip state syncs. Setting this interval lower (more frequent) will increase convergence speeds across larger clusters at the expense of increased bandwidth usage.").Default(cluster.DefaultPushPullInterval.String()).Duration() tcpTimeout = kingpin.Flag("cluster.tcp-timeout", "Timeout for establishing a stream connection with a remote node for a full state sync, and for stream read and write operations.").Default(cluster.DefaultTcpTimeout.String()).Duration() probeTimeout = kingpin.Flag("cluster.probe-timeout", "Timeout to wait for an ack from a probed node before assuming it is unhealthy. This should be set to 99-percentile of RTT (round-trip time) on your network.").Default(cluster.DefaultProbeTimeout.String()).Duration() probeInterval = kingpin.Flag("cluster.probe-interval", "Interval between random node probes. Setting this lower (more frequent) will cause the cluster to detect failed nodes more quickly at the expense of increased bandwidth usage.").Default(cluster.DefaultProbeInterval.String()).Duration() settleTimeout = kingpin.Flag("cluster.settle-timeout", "Maximum time to wait for cluster connections to settle before evaluating notifications.").Default(cluster.DefaultPushPullInterval.String()).Duration() reconnectInterval = kingpin.Flag("cluster.reconnect-interval", "Interval between attempting to reconnect to lost peers.").Default(cluster.DefaultReconnectInterval.String()).Duration() peerReconnectTimeout = kingpin.Flag("cluster.reconnect-timeout", "Length of time to attempt to reconnect to a lost peer.").Default(cluster.DefaultReconnectTimeout.String()).Duration() ) kingpin.Version(version.Print("alertmanager")) kingpin.CommandLine.GetFlag("help").Short('h') kingpin.Parse() logLevel.Set(*logLevelString) logger := promlog.New(*logLevel) level.Info(logger).Log("msg", "Starting Alertmanager", "version", version.Info()) level.Info(logger).Log("build_context", version.BuildContext()) err := os.MkdirAll(*dataDir, 0777) if err != nil { level.Error(logger).Log("msg", "Unable to create data directory", "err", err) os.Exit(1) } var peer *cluster.Peer if *clusterBindAddr != "" { peer, err = cluster.Create( log.With(logger, "component", "cluster"), prometheus.DefaultRegisterer, *clusterBindAddr, *clusterAdvertiseAddr, *peers, true, *pushPullInterval, *gossipInterval, *tcpTimeout, *probeTimeout, *probeInterval, ) if err != nil { level.Error(logger).Log("msg", "unable to initialize gossip mesh", "err", err) os.Exit(1) } } stopc := make(chan struct{}) var wg sync.WaitGroup wg.Add(1) notificationLogOpts := []nflog.Option{ nflog.WithRetention(*retention), nflog.WithSnapshot(filepath.Join(*dataDir, "nflog")), nflog.WithMaintenance(15*time.Minute, stopc, wg.Done), nflog.WithMetrics(prometheus.DefaultRegisterer), nflog.WithLogger(log.With(logger, "component", "nflog")), } notificationLog, err := nflog.New(notificationLogOpts...) if err != nil { level.Error(logger).Log("err", err) os.Exit(1) } if peer != nil { c := peer.AddState("nfl", notificationLog, prometheus.DefaultRegisterer) notificationLog.SetBroadcast(c.Broadcast) } marker := types.NewMarker() newMarkerMetrics(marker) silenceOpts := silence.Options{ SnapshotFile: filepath.Join(*dataDir, "silences"), Retention: *retention, Logger: log.With(logger, "component", "silences"), Metrics: prometheus.DefaultRegisterer, } silences, err := silence.New(silenceOpts) if err != nil { level.Error(logger).Log("err", err) os.Exit(1) } if peer != nil { c := peer.AddState("sil", silences, prometheus.DefaultRegisterer) silences.SetBroadcast(c.Broadcast) } // Start providers before router potentially sends updates. wg.Add(1) go func() { silences.Maintenance(15*time.Minute, filepath.Join(*dataDir, "silences"), stopc) wg.Done() }() defer func() { close(stopc) wg.Wait() }() // Peer state listeners have been registered, now we can join and get the initial state. if peer != nil { err = peer.Join( *reconnectInterval, *peerReconnectTimeout, ) if err != nil { level.Warn(logger).Log("msg", "unable to join gossip mesh", "err", err) } ctx, cancel := context.WithTimeout(context.Background(), *settleTimeout) defer func() { cancel() if err := peer.Leave(10 * time.Second); err != nil { level.Warn(logger).Log("msg", "unable to leave gossip mesh", "err", err) } }() go peer.Settle(ctx, *gossipInterval*10) } alerts, err := mem.NewAlerts(marker, *alertGCInterval) if err != nil { level.Error(logger).Log("err", err) os.Exit(1) } defer alerts.Close() var ( inhibitor *inhibit.Inhibitor tmpl *template.Template pipeline notify.Stage disp *dispatch.Dispatcher ) defer disp.Stop() apiv := api.New( alerts, silences, func(matchers []*labels.Matcher) dispatch.AlertOverview { return disp.Groups(matchers) }, marker.Status, peer, logger, ) amURL, err := extURL(*listenAddress, *externalURL) if err != nil { level.Error(logger).Log("err", err) os.Exit(1) } waitFunc := func() time.Duration { return 0 } if peer != nil { waitFunc = clusterWait(peer, *peerTimeout) } timeoutFunc := func(d time.Duration) time.Duration { if d < notify.MinTimeout { d = notify.MinTimeout } return d + waitFunc() } var hash float64 reload := func() (err error) { level.Info(logger).Log("msg", "Loading configuration file", "file", *configFile) defer func() { if err != nil { level.Error(logger).Log("msg", "Loading configuration file failed", "file", *configFile, "err", err) configSuccess.Set(0) } else { configSuccess.Set(1) configSuccessTime.Set(float64(time.Now().Unix())) configHash.Set(hash) } }() conf, plainCfg, err := config.LoadFile(*configFile) if err != nil { return err } hash = md5HashAsMetricValue(plainCfg) err = apiv.Update(conf, time.Duration(conf.Global.ResolveTimeout)) if err != nil { return err } tmpl, err = template.FromGlobs(conf.Templates...) if err != nil { return err } tmpl.ExternalURL = amURL inhibitor.Stop() disp.Stop() inhibitor = inhibit.NewInhibitor(alerts, conf.InhibitRules, marker, logger) pipeline = notify.BuildPipeline( conf.Receivers, tmpl, waitFunc, inhibitor, silences, notificationLog, marker, peer, logger, ) disp = dispatch.NewDispatcher(alerts, dispatch.NewRoute(conf.Route, nil), pipeline, marker, timeoutFunc, logger) go disp.Run() go inhibitor.Run() return nil } if err := reload(); err != nil { os.Exit(1) } // Make routePrefix default to externalURL path if empty string. if routePrefix == nil || *routePrefix == "" { *routePrefix = amURL.Path } *routePrefix = "/" + strings.Trim(*routePrefix, "/") router := route.New().WithInstrumentation(instrumentHandler) if *routePrefix != "/" { router = router.WithPrefix(*routePrefix) } webReload := make(chan chan error) ui.Register(router, webReload, logger) apiv.Register(router.WithPrefix("/api/v1")) level.Info(logger).Log("msg", "Listening", "address", *listenAddress) go listen(*listenAddress, router, logger) var ( hup = make(chan os.Signal) hupReady = make(chan bool) term = make(chan os.Signal, 1) ) signal.Notify(hup, syscall.SIGHUP) signal.Notify(term, os.Interrupt, syscall.SIGTERM) go func() { <-hupReady for { select { case <-hup: reload() case errc := <-webReload: errc <- reload() } } }() // Wait for reload or termination signals. close(hupReady) // Unblock SIGHUP handler. <-term level.Info(logger).Log("msg", "Received SIGTERM, exiting gracefully...") } // clusterWait returns a function that inspects the current peer state and returns // a duration of one base timeout for each peer with a higher ID than ourselves. func clusterWait(p *cluster.Peer, timeout time.Duration) func() time.Duration { return func() time.Duration { return time.Duration(p.Position()) * timeout } } func extURL(listen, external string) (*url.URL, error) { if external == "" { hostname, err := os.Hostname() if err != nil { return nil, err } _, port, err := net.SplitHostPort(listen) if err != nil { return nil, err } external = fmt.Sprintf("http://%s:%s/", hostname, port) } u, err := url.Parse(external) if err != nil { return nil, err } ppref := strings.TrimRight(u.Path, "/") if ppref != "" && !strings.HasPrefix(ppref, "/") { ppref = "/" + ppref } u.Path = ppref return u, nil } func listen(listen string, router *route.Router, logger log.Logger) { if err := http.ListenAndServe(listen, router); err != nil { level.Error(logger).Log("msg", "Listen error", "err", err) os.Exit(1) } } func md5HashAsMetricValue(data []byte) float64 { sum := md5.Sum(data) // We only want 48 bits as a float64 only has a 53 bit mantissa. smallSum := sum[0:6] var bytes = make([]byte, 8) copy(bytes, smallSum) return float64(binary.LittleEndian.Uint64(bytes)) } prometheus-alertmanager-0.15.3+ds/cmd/amtool/000077500000000000000000000000001341674552200211045ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/cmd/amtool/README.md000066400000000000000000000010761341674552200223670ustar00rootroot00000000000000# Generating amtool artifacts Amtool comes with the option to create a number of ease-of-use artifacts that can be created. ## Shell completion A bash completion script can be generated by calling `amtool --completion-script-bash`. The bash completion file can be added to `/etc/bash_completion.d/`. ## Man pages A man page can be generated by calling `amtool --help-man`. Man pages can be added to the man directory of your choice amtool --help-man > /usr/local/share/man/man1/amtool.1 sudo mandb Then you should be able to view the man pages as expected. prometheus-alertmanager-0.15.3+ds/cmd/amtool/main.go000066400000000000000000000012501341674552200223550ustar00rootroot00000000000000// Copyright 2018 Prometheus Team // 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 "github.com/prometheus/alertmanager/cli" func main() { cli.Execute() } prometheus-alertmanager-0.15.3+ds/config/000077500000000000000000000000001341674552200203135ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/config/config.go000066400000000000000000000505671341674552200221240ustar00rootroot00000000000000// Copyright 2015 Prometheus Team // 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 config import ( "encoding/json" "errors" "fmt" "io/ioutil" "net/url" "path/filepath" "regexp" "strings" "time" commoncfg "github.com/prometheus/common/config" "github.com/prometheus/common/model" "gopkg.in/yaml.v2" ) // Secret is a string that must not be revealed on marshaling. type Secret string // MarshalYAML implements the yaml.Marshaler interface. func (s Secret) MarshalYAML() (interface{}, error) { if s != "" { return "", nil } return nil, nil } // UnmarshalYAML implements the yaml.Unmarshaler interface for Secret. func (s *Secret) UnmarshalYAML(unmarshal func(interface{}) error) error { type plain Secret return unmarshal((*plain)(s)) } // MarshalJSON implements the json.Marshaler interface. func (s Secret) MarshalJSON() ([]byte, error) { return json.Marshal("") } // URL is a custom type that allows validation at configuration load time. type URL struct { *url.URL } // Copy makes a deep-copy of the struct. func (u *URL) Copy() *URL { v := *u.URL return &URL{&v} } // MarshalYAML implements the yaml.Marshaler interface for URL. func (u URL) MarshalYAML() (interface{}, error) { if u.URL != nil { return u.URL.String(), nil } return nil, nil } // UnmarshalYAML implements the yaml.Unmarshaler interface for URL. func (u *URL) UnmarshalYAML(unmarshal func(interface{}) error) error { var s string if err := unmarshal(&s); err != nil { return err } urlp, err := url.Parse(s) if err != nil { return err } u.URL = urlp return nil } // MarshalJSON implements the json.Marshaler interface for URL. func (u URL) MarshalJSON() ([]byte, error) { if u.URL != nil { return json.Marshal(u.URL.String()) } return nil, nil } // UnmarshalJSON implements the json.Marshaler interface for URL. func (u *URL) UnmarshalJSON(data []byte) error { var s string if err := json.Unmarshal(data, &s); err != nil { return err } urlp, err := url.Parse(s) if err != nil { return err } u.URL = urlp return nil } // SecretURL is a URL that must not be revealed on marshaling. type SecretURL URL // MarshalYAML implements the yaml.Marshaler interface for SecretURL. func (s SecretURL) MarshalYAML() (interface{}, error) { if s.URL != nil { return "", nil } return nil, nil } // UnmarshalYAML implements the yaml.Unmarshaler interface for SecretURL. func (s *SecretURL) UnmarshalYAML(unmarshal func(interface{}) error) error { return unmarshal((*URL)(s)) } // MarshalJSON implements the json.Marshaler interface for SecretURL. func (s SecretURL) MarshalJSON() ([]byte, error) { return json.Marshal("") } // UnmarshalJSON implements the json.Marshaler interface for SecretURL. func (s *SecretURL) UnmarshalJSON(data []byte) error { return json.Unmarshal(data, (*URL)(s)) } // Load parses the YAML input s into a Config. func Load(s string) (*Config, error) { cfg := &Config{} err := yaml.UnmarshalStrict([]byte(s), cfg) if err != nil { return nil, err } // Check if we have a root route. We cannot check for it in the // UnmarshalYAML method because it won't be called if the input is empty // (e.g. the config file is empty or only contains whitespace). if cfg.Route == nil { return nil, errors.New("no route provided in config") } // Check if continue in root route. if cfg.Route.Continue { return nil, errors.New("cannot have continue in root route") } cfg.original = s return cfg, nil } // LoadFile parses the given YAML file into a Config. func LoadFile(filename string) (*Config, []byte, error) { content, err := ioutil.ReadFile(filename) if err != nil { return nil, nil, err } cfg, err := Load(string(content)) if err != nil { return nil, nil, err } resolveFilepaths(filepath.Dir(filename), cfg) return cfg, content, nil } // resolveFilepaths joins all relative paths in a configuration // with a given base directory. func resolveFilepaths(baseDir string, cfg *Config) { join := func(fp string) string { if len(fp) > 0 && !filepath.IsAbs(fp) { fp = filepath.Join(baseDir, fp) } return fp } for i, tf := range cfg.Templates { cfg.Templates[i] = join(tf) } } // Config is the top-level configuration for Alertmanager's config files. type Config struct { Global *GlobalConfig `yaml:"global,omitempty" json:"global,omitempty"` Route *Route `yaml:"route,omitempty" json:"route,omitempty"` InhibitRules []*InhibitRule `yaml:"inhibit_rules,omitempty" json:"inhibit_rules,omitempty"` Receivers []*Receiver `yaml:"receivers,omitempty" json:"receivers,omitempty"` Templates []string `yaml:"templates" json:"templates"` // original is the input from which the config was parsed. original string } func (c Config) String() string { b, err := yaml.Marshal(c) if err != nil { return fmt.Sprintf("", err) } return string(b) } // UnmarshalYAML implements the yaml.Unmarshaler interface. func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { // We want to set c to the defaults and then overwrite it with the input. // To make unmarshal fill the plain data struct rather than calling UnmarshalYAML // again, we have to hide it using a type indirection. type plain Config if err := unmarshal((*plain)(c)); err != nil { return err } // If a global block was open but empty the default global config is overwritten. // We have to restore it here. if c.Global == nil { c.Global = &GlobalConfig{} *c.Global = DefaultGlobalConfig } names := map[string]struct{}{} for _, rcv := range c.Receivers { if _, ok := names[rcv.Name]; ok { return fmt.Errorf("notification config name %q is not unique", rcv.Name) } for _, wh := range rcv.WebhookConfigs { if wh.HTTPConfig == nil { wh.HTTPConfig = c.Global.HTTPConfig } } for _, ec := range rcv.EmailConfigs { if ec.Smarthost == "" { if c.Global.SMTPSmarthost == "" { return fmt.Errorf("no global SMTP smarthost set") } ec.Smarthost = c.Global.SMTPSmarthost } if ec.From == "" { if c.Global.SMTPFrom == "" { return fmt.Errorf("no global SMTP from set") } ec.From = c.Global.SMTPFrom } if ec.Hello == "" { ec.Hello = c.Global.SMTPHello } if ec.AuthUsername == "" { ec.AuthUsername = c.Global.SMTPAuthUsername } if ec.AuthPassword == "" { ec.AuthPassword = c.Global.SMTPAuthPassword } if ec.AuthSecret == "" { ec.AuthSecret = c.Global.SMTPAuthSecret } if ec.AuthIdentity == "" { ec.AuthIdentity = c.Global.SMTPAuthIdentity } if ec.RequireTLS == nil { ec.RequireTLS = new(bool) *ec.RequireTLS = c.Global.SMTPRequireTLS } } for _, sc := range rcv.SlackConfigs { if sc.HTTPConfig == nil { sc.HTTPConfig = c.Global.HTTPConfig } if sc.APIURL == nil { if c.Global.SlackAPIURL == nil { return fmt.Errorf("no global Slack API URL set") } sc.APIURL = c.Global.SlackAPIURL } } for _, hc := range rcv.HipchatConfigs { if hc.HTTPConfig == nil { hc.HTTPConfig = c.Global.HTTPConfig } if hc.APIURL == nil { if c.Global.HipchatAPIURL == nil { return fmt.Errorf("no global Hipchat API URL set") } hc.APIURL = c.Global.HipchatAPIURL } if !strings.HasSuffix(hc.APIURL.Path, "/") { hc.APIURL.Path += "/" } if hc.AuthToken == "" { if c.Global.HipchatAuthToken == "" { return fmt.Errorf("no global Hipchat Auth Token set") } hc.AuthToken = c.Global.HipchatAuthToken } } for _, poc := range rcv.PushoverConfigs { if poc.HTTPConfig == nil { poc.HTTPConfig = c.Global.HTTPConfig } } for _, pdc := range rcv.PagerdutyConfigs { if pdc.HTTPConfig == nil { pdc.HTTPConfig = c.Global.HTTPConfig } if pdc.URL == nil { if c.Global.PagerdutyURL == nil { return fmt.Errorf("no global PagerDuty URL set") } pdc.URL = c.Global.PagerdutyURL } } for _, ogc := range rcv.OpsGenieConfigs { if ogc.HTTPConfig == nil { ogc.HTTPConfig = c.Global.HTTPConfig } if ogc.APIURL == nil { if c.Global.OpsGenieAPIURL == nil { return fmt.Errorf("no global OpsGenie URL set") } ogc.APIURL = c.Global.OpsGenieAPIURL } if !strings.HasSuffix(ogc.APIURL.Path, "/") { ogc.APIURL.Path += "/" } if ogc.APIKey == "" { if c.Global.OpsGenieAPIKey == "" { return fmt.Errorf("no global OpsGenie API Key set") } ogc.APIKey = c.Global.OpsGenieAPIKey } } for _, wcc := range rcv.WechatConfigs { if wcc.HTTPConfig == nil { wcc.HTTPConfig = c.Global.HTTPConfig } if wcc.APIURL == nil { if c.Global.WeChatAPIURL == nil { return fmt.Errorf("no global Wechat URL set") } wcc.APIURL = c.Global.WeChatAPIURL } if wcc.APISecret == "" { if c.Global.WeChatAPISecret == "" { return fmt.Errorf("no global Wechat ApiSecret set") } wcc.APISecret = c.Global.WeChatAPISecret } if wcc.CorpID == "" { if c.Global.WeChatAPICorpID == "" { return fmt.Errorf("no global Wechat CorpID set") } wcc.CorpID = c.Global.WeChatAPICorpID } if !strings.HasSuffix(wcc.APIURL.Path, "/") { wcc.APIURL.Path += "/" } } for _, voc := range rcv.VictorOpsConfigs { if voc.HTTPConfig == nil { voc.HTTPConfig = c.Global.HTTPConfig } if voc.APIURL == nil { if c.Global.VictorOpsAPIURL == nil { return fmt.Errorf("no global VictorOps URL set") } voc.APIURL = c.Global.VictorOpsAPIURL } if !strings.HasSuffix(voc.APIURL.Path, "/") { voc.APIURL.Path += "/" } if voc.APIKey == "" { if c.Global.VictorOpsAPIKey == "" { return fmt.Errorf("no global VictorOps API Key set") } voc.APIKey = c.Global.VictorOpsAPIKey } } names[rcv.Name] = struct{}{} } // The root route must not have any matchers as it is the fallback node // for all alerts. if c.Route == nil { return fmt.Errorf("no routes provided") } if len(c.Route.Receiver) == 0 { return fmt.Errorf("root route must specify a default receiver") } if len(c.Route.Match) > 0 || len(c.Route.MatchRE) > 0 { return fmt.Errorf("root route must not have any matchers") } // Validate that all receivers used in the routing tree are defined. return checkReceiver(c.Route, names) } // checkReceiver returns an error if a node in the routing tree // references a receiver not in the given map. func checkReceiver(r *Route, receivers map[string]struct{}) error { if r.Receiver == "" { return nil } if _, ok := receivers[r.Receiver]; !ok { return fmt.Errorf("undefined receiver %q used in route", r.Receiver) } for _, sr := range r.Routes { if err := checkReceiver(sr, receivers); err != nil { return err } } return nil } // DefaultGlobalConfig provides global default values. var DefaultGlobalConfig = GlobalConfig{ ResolveTimeout: model.Duration(5 * time.Minute), HTTPConfig: &commoncfg.HTTPClientConfig{}, SMTPHello: "localhost", SMTPRequireTLS: true, PagerdutyURL: mustParseURL("https://events.pagerduty.com/v2/enqueue"), HipchatAPIURL: mustParseURL("https://api.hipchat.com/"), OpsGenieAPIURL: mustParseURL("https://api.opsgenie.com/"), WeChatAPIURL: mustParseURL("https://qyapi.weixin.qq.com/cgi-bin/"), VictorOpsAPIURL: mustParseURL("https://alert.victorops.com/integrations/generic/20131114/alert/"), } func mustParseURL(s string) *URL { u, err := url.Parse(s) if err != nil { panic(err) } return &URL{u} } // GlobalConfig defines configuration parameters that are valid globally // unless overwritten. type GlobalConfig struct { // ResolveTimeout is the time after which an alert is declared resolved // if it has not been updated. ResolveTimeout model.Duration `yaml:"resolve_timeout" json:"resolve_timeout"` HTTPConfig *commoncfg.HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"` SMTPFrom string `yaml:"smtp_from,omitempty" json:"smtp_from,omitempty"` SMTPHello string `yaml:"smtp_hello,omitempty" json:"smtp_hello,omitempty"` SMTPSmarthost string `yaml:"smtp_smarthost,omitempty" json:"smtp_smarthost,omitempty"` SMTPAuthUsername string `yaml:"smtp_auth_username,omitempty" json:"smtp_auth_username,omitempty"` SMTPAuthPassword Secret `yaml:"smtp_auth_password,omitempty" json:"smtp_auth_password,omitempty"` SMTPAuthSecret Secret `yaml:"smtp_auth_secret,omitempty" json:"smtp_auth_secret,omitempty"` SMTPAuthIdentity string `yaml:"smtp_auth_identity,omitempty" json:"smtp_auth_identity,omitempty"` SMTPRequireTLS bool `yaml:"smtp_require_tls,omitempty" json:"smtp_require_tls,omitempty"` SlackAPIURL *SecretURL `yaml:"slack_api_url,omitempty" json:"slack_api_url,omitempty"` PagerdutyURL *URL `yaml:"pagerduty_url,omitempty" json:"pagerduty_url,omitempty"` HipchatAPIURL *URL `yaml:"hipchat_api_url,omitempty" json:"hipchat_api_url,omitempty"` HipchatAuthToken Secret `yaml:"hipchat_auth_token,omitempty" json:"hipchat_auth_token,omitempty"` OpsGenieAPIURL *URL `yaml:"opsgenie_api_url,omitempty" json:"opsgenie_api_url,omitempty"` OpsGenieAPIKey Secret `yaml:"opsgenie_api_key,omitempty" json:"opsgenie_api_key,omitempty"` WeChatAPIURL *URL `yaml:"wechat_api_url,omitempty" json:"wechat_api_url,omitempty"` WeChatAPISecret Secret `yaml:"wechat_api_secret,omitempty" json:"wechat_api_secret,omitempty"` WeChatAPICorpID string `yaml:"wechat_api_corp_id,omitempty" json:"wechat_api_corp_id,omitempty"` VictorOpsAPIURL *URL `yaml:"victorops_api_url,omitempty" json:"victorops_api_url,omitempty"` VictorOpsAPIKey Secret `yaml:"victorops_api_key,omitempty" json:"victorops_api_key,omitempty"` } // UnmarshalYAML implements the yaml.Unmarshaler interface. func (c *GlobalConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { *c = DefaultGlobalConfig type plain GlobalConfig return unmarshal((*plain)(c)) } // A Route is a node that contains definitions of how to handle alerts. type Route struct { Receiver string `yaml:"receiver,omitempty" json:"receiver,omitempty"` GroupBy []model.LabelName `yaml:"group_by,omitempty" json:"group_by,omitempty"` Match map[string]string `yaml:"match,omitempty" json:"match,omitempty"` MatchRE map[string]Regexp `yaml:"match_re,omitempty" json:"match_re,omitempty"` Continue bool `yaml:"continue,omitempty" json:"continue,omitempty"` Routes []*Route `yaml:"routes,omitempty" json:"routes,omitempty"` GroupWait *model.Duration `yaml:"group_wait,omitempty" json:"group_wait,omitempty"` GroupInterval *model.Duration `yaml:"group_interval,omitempty" json:"group_interval,omitempty"` RepeatInterval *model.Duration `yaml:"repeat_interval,omitempty" json:"repeat_interval,omitempty"` } // UnmarshalYAML implements the yaml.Unmarshaler interface. func (r *Route) UnmarshalYAML(unmarshal func(interface{}) error) error { type plain Route if err := unmarshal((*plain)(r)); err != nil { return err } for k := range r.Match { if !model.LabelNameRE.MatchString(k) { return fmt.Errorf("invalid label name %q", k) } } for k := range r.MatchRE { if !model.LabelNameRE.MatchString(k) { return fmt.Errorf("invalid label name %q", k) } } groupBy := map[model.LabelName]struct{}{} for _, ln := range r.GroupBy { if _, ok := groupBy[ln]; ok { return fmt.Errorf("duplicated label %q in group_by", ln) } groupBy[ln] = struct{}{} } if r.GroupInterval != nil && time.Duration(*r.GroupInterval) == time.Duration(0) { return fmt.Errorf("group_interval cannot be zero") } if r.RepeatInterval != nil && time.Duration(*r.RepeatInterval) == time.Duration(0) { return fmt.Errorf("repeat_interval cannot be zero") } return nil } // InhibitRule defines an inhibition rule that mutes alerts that match the // target labels if an alert matching the source labels exists. // Both alerts have to have a set of labels being equal. type InhibitRule struct { // SourceMatch defines a set of labels that have to equal the given // value for source alerts. SourceMatch map[string]string `yaml:"source_match,omitempty" json:"source_match,omitempty"` // SourceMatchRE defines pairs like SourceMatch but does regular expression // matching. SourceMatchRE map[string]Regexp `yaml:"source_match_re,omitempty" json:"source_match_re,omitempty"` // TargetMatch defines a set of labels that have to equal the given // value for target alerts. TargetMatch map[string]string `yaml:"target_match,omitempty" json:"target_match,omitempty"` // TargetMatchRE defines pairs like TargetMatch but does regular expression // matching. TargetMatchRE map[string]Regexp `yaml:"target_match_re,omitempty" json:"target_match_re,omitempty"` // A set of labels that must be equal between the source and target alert // for them to be a match. Equal model.LabelNames `yaml:"equal,omitempty" json:"equal,omitempty"` } // UnmarshalYAML implements the yaml.Unmarshaler interface. func (r *InhibitRule) UnmarshalYAML(unmarshal func(interface{}) error) error { type plain InhibitRule if err := unmarshal((*plain)(r)); err != nil { return err } for k := range r.SourceMatch { if !model.LabelNameRE.MatchString(k) { return fmt.Errorf("invalid label name %q", k) } } for k := range r.SourceMatchRE { if !model.LabelNameRE.MatchString(k) { return fmt.Errorf("invalid label name %q", k) } } for k := range r.TargetMatch { if !model.LabelNameRE.MatchString(k) { return fmt.Errorf("invalid label name %q", k) } } for k := range r.TargetMatchRE { if !model.LabelNameRE.MatchString(k) { return fmt.Errorf("invalid label name %q", k) } } return nil } // Receiver configuration provides configuration on how to contact a receiver. type Receiver struct { // A unique identifier for this receiver. Name string `yaml:"name" json:"name"` EmailConfigs []*EmailConfig `yaml:"email_configs,omitempty" json:"email_configs,omitempty"` PagerdutyConfigs []*PagerdutyConfig `yaml:"pagerduty_configs,omitempty" json:"pagerduty_configs,omitempty"` HipchatConfigs []*HipchatConfig `yaml:"hipchat_configs,omitempty" json:"hipchat_configs,omitempty"` SlackConfigs []*SlackConfig `yaml:"slack_configs,omitempty" json:"slack_configs,omitempty"` WebhookConfigs []*WebhookConfig `yaml:"webhook_configs,omitempty" json:"webhook_configs,omitempty"` OpsGenieConfigs []*OpsGenieConfig `yaml:"opsgenie_configs,omitempty" json:"opsgenie_configs,omitempty"` WechatConfigs []*WechatConfig `yaml:"wechat_configs,omitempty" json:"wechat_configs,omitempty"` PushoverConfigs []*PushoverConfig `yaml:"pushover_configs,omitempty" json:"pushover_configs,omitempty"` VictorOpsConfigs []*VictorOpsConfig `yaml:"victorops_configs,omitempty" json:"victorops_configs,omitempty"` } // UnmarshalYAML implements the yaml.Unmarshaler interface. func (c *Receiver) UnmarshalYAML(unmarshal func(interface{}) error) error { type plain Receiver if err := unmarshal((*plain)(c)); err != nil { return err } if c.Name == "" { return fmt.Errorf("missing name in receiver") } return nil } // Regexp encapsulates a regexp.Regexp and makes it YAML marshalable. type Regexp struct { *regexp.Regexp } // UnmarshalYAML implements the yaml.Unmarshaler interface. func (re *Regexp) UnmarshalYAML(unmarshal func(interface{}) error) error { var s string if err := unmarshal(&s); err != nil { return err } regex, err := regexp.Compile("^(?:" + s + ")$") if err != nil { return err } re.Regexp = regex return nil } // MarshalYAML implements the yaml.Marshaler interface. func (re Regexp) MarshalYAML() (interface{}, error) { if re.Regexp != nil { return re.String(), nil } return nil, nil } // UnmarshalJSON implements the json.Marshaler interface func (re *Regexp) UnmarshalJSON(data []byte) error { var s string if err := json.Unmarshal(data, &s); err != nil { return err } regex, err := regexp.Compile("^(?:" + s + ")$") if err != nil { return err } re.Regexp = regex return nil } // MarshalJSON implements the json.Marshaler interface. func (re Regexp) MarshalJSON() ([]byte, error) { if re.Regexp != nil { return json.Marshal(re.String()) } return nil, nil } prometheus-alertmanager-0.15.3+ds/config/config_test.go000066400000000000000000000311101341674552200231420ustar00rootroot00000000000000// Copyright 2016 Prometheus Team // 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 config import ( "encoding/json" "net/url" "reflect" "regexp" "strings" "testing" "time" commoncfg "github.com/prometheus/common/config" "github.com/prometheus/common/model" "github.com/stretchr/testify/require" "gopkg.in/yaml.v2" ) func TestLoadEmptyString(t *testing.T) { var in string _, err := Load(in) expected := "no route provided in config" if err == nil { t.Fatalf("no error returned, expected:\n%v", expected) } if err.Error() != expected { t.Errorf("\nexpected:\n%v\ngot:\n%v", expected, err.Error()) } } func TestDefaultReceiverExists(t *testing.T) { in := ` route: group_wait: 30s ` _, err := Load(in) expected := "root route must specify a default receiver" if err == nil { t.Fatalf("no error returned, expected:\n%v", expected) } if err.Error() != expected { t.Errorf("\nexpected:\n%v\ngot:\n%v", expected, err.Error()) } } func TestReceiverNameIsUnique(t *testing.T) { in := ` route: receiver: team-X receivers: - name: 'team-X' - name: 'team-X' ` _, err := Load(in) expected := "notification config name \"team-X\" is not unique" if err == nil { t.Fatalf("no error returned, expected:\n%q", expected) } if err.Error() != expected { t.Errorf("\nexpected:\n%q\ngot:\n%q", expected, err.Error()) } } func TestReceiverExists(t *testing.T) { in := ` route: receiver: team-X receivers: - name: 'team-Y' ` _, err := Load(in) expected := "undefined receiver \"team-X\" used in route" if err == nil { t.Fatalf("no error returned, expected:\n%q", expected) } if err.Error() != expected { t.Errorf("\nexpected:\n%q\ngot:\n%q", expected, err.Error()) } } func TestReceiverHasName(t *testing.T) { in := ` route: receivers: - name: '' ` _, err := Load(in) expected := "missing name in receiver" if err == nil { t.Fatalf("no error returned, expected:\n%q", expected) } if err.Error() != expected { t.Errorf("\nexpected:\n%q\ngot:\n%q", expected, err.Error()) } } func TestGroupByHasNoDuplicatedLabels(t *testing.T) { in := ` route: group_by: ['alertname', 'cluster', 'service', 'cluster'] receivers: - name: 'team-X-mails' ` _, err := Load(in) expected := "duplicated label \"cluster\" in group_by" if err == nil { t.Fatalf("no error returned, expected:\n%q", expected) } if err.Error() != expected { t.Errorf("\nexpected:\n%q\ngot:\n%q", expected, err.Error()) } } func TestRootRouteExists(t *testing.T) { in := ` receivers: - name: 'team-X-mails' ` _, err := Load(in) expected := "no routes provided" if err == nil { t.Fatalf("no error returned, expected:\n%q", expected) } if err.Error() != expected { t.Errorf("\nexpected:\n%q\ngot:\n%q", expected, err.Error()) } } func TestRootRouteHasNoMatcher(t *testing.T) { in := ` route: receiver: 'team-X' match: severity: critical receivers: - name: 'team-X' ` _, err := Load(in) expected := "root route must not have any matchers" if err == nil { t.Fatalf("no error returned, expected:\n%q", expected) } if err.Error() != expected { t.Errorf("\nexpected:\n%q\ngot:\n%q", expected, err.Error()) } } func TestContinueErrorInRouteRoot(t *testing.T) { in := ` route: receiver: team-X-mails continue: true receivers: - name: 'team-X-mails' ` _, err := Load(in) expected := "cannot have continue in root route" if err == nil { t.Fatalf("no error returned, expected:\n%q", expected) } if err.Error() != expected { t.Errorf("\nexpected:\n%q\ngot:\n%q", expected, err.Error()) } } func TestGroupIntervalIsGreaterThanZero(t *testing.T) { in := ` route: receiver: team-X-mails group_interval: 0s receivers: - name: 'team-X-mails' ` _, err := Load(in) expected := "group_interval cannot be zero" if err == nil { t.Fatalf("no error returned, expected:\n%q", expected) } if err.Error() != expected { t.Errorf("\nexpected:\n%q\ngot:\n%q", expected, err.Error()) } } func TestRepeatIntervalIsGreaterThanZero(t *testing.T) { in := ` route: receiver: team-X-mails repeat_interval: 0s receivers: - name: 'team-X-mails' ` _, err := Load(in) expected := "repeat_interval cannot be zero" if err == nil { t.Fatalf("no error returned, expected:\n%q", expected) } if err.Error() != expected { t.Errorf("\nexpected:\n%q\ngot:\n%q", expected, err.Error()) } } func TestHideConfigSecrets(t *testing.T) { c, _, err := LoadFile("testdata/conf.good.yml") if err != nil { t.Fatalf("Error parsing %s: %s", "testdata/conf.good.yml", err) } // String method must not reveal authentication credentials. s := c.String() if strings.Count(s, "") != 14 || strings.Contains(s, "mysecret") { t.Fatal("config's String method reveals authentication credentials.") } } func TestJSONMarshal(t *testing.T) { c, _, err := LoadFile("testdata/conf.good.yml") if err != nil { t.Errorf("Error parsing %s: %s", "testdata/conf.good.yml", err) } _, err = json.Marshal(c) if err != nil { t.Fatal("JSON Marshaling failed:", err) } } func TestJSONMarshalSecret(t *testing.T) { test := struct { S Secret }{ S: Secret("test"), } c, err := json.Marshal(test) if err != nil { t.Fatal(err) } // u003c -> "<" // u003e -> ">" require.Equal(t, "{\"S\":\"\\u003csecret\\u003e\"}", string(c), "Secret not properly elided.") } func TestMarshalSecretURL(t *testing.T) { urlp, err := url.Parse("http://example.com/") if err != nil { t.Fatal(err) } u := &SecretURL{urlp} c, err := json.Marshal(u) if err != nil { t.Fatal(err) } // u003c -> "<" // u003e -> ">" require.Equal(t, "\"\\u003csecret\\u003e\"", string(c), "SecretURL not properly elided in JSON.") c, err = yaml.Marshal(u) if err != nil { t.Fatal(err) } require.Equal(t, "\n", string(c), "SecretURL not properly elided in YAML.") } func TestUnmarshalSecretURL(t *testing.T) { b := []byte(`"http://example.com/se cret"`) var u SecretURL err := json.Unmarshal(b, &u) if err != nil { t.Fatal(err) } require.Equal(t, "http://example.com/se%20cret", u.String(), "SecretURL not properly unmarshalled in JSON.") err = yaml.Unmarshal(b, &u) if err != nil { t.Fatal(err) } require.Equal(t, "http://example.com/se%20cret", u.String(), "SecretURL not properly unmarshalled in YAML.") } func TestMarshalURL(t *testing.T) { urlp, err := url.Parse("http://example.com/") if err != nil { t.Fatal(err) } u := &URL{urlp} c, err := json.Marshal(u) if err != nil { t.Fatal(err) } require.Equal(t, "\"http://example.com/\"", string(c), "URL not properly marshalled in JSON.") c, err = yaml.Marshal(u) if err != nil { t.Fatal(err) } require.Equal(t, "http://example.com/\n", string(c), "URL not properly marshalled in YAML.") } func TestUnmarshalURL(t *testing.T) { b := []byte(`"http://example.com/a b"`) var u URL err := json.Unmarshal(b, &u) if err != nil { t.Fatal(err) } require.Equal(t, "http://example.com/a%20b", u.String(), "URL not properly unmarshalled in JSON.") err = json.Unmarshal(b, &u) if err != nil { t.Fatal(err) } require.Equal(t, "http://example.com/a%20b", u.String(), "URL not properly unmarshalled in YAML.") } func TestUnmarshalInvalidURL(t *testing.T) { b := []byte(`"://example.com"`) var u URL err := json.Unmarshal(b, &u) if err == nil { t.Errorf("Expected an error parsing URL") } err = yaml.Unmarshal(b, &u) if err == nil { t.Errorf("Expected an error parsing URL") } } func TestJSONUnmarshalMarshaled(t *testing.T) { c, _, err := LoadFile("testdata/conf.good.yml") if err != nil { t.Errorf("Error parsing %s: %s", "testdata/conf.good.yml", err) } plainCfg, err := json.Marshal(c) if err != nil { t.Fatal("JSON Marshaling failed:", err) } cfg := Config{} err = json.Unmarshal(plainCfg, &cfg) if err != nil { t.Fatal("JSON Unmarshaling failed:", err) } } func TestEmptyFieldsAndRegex(t *testing.T) { boolFoo := true var regexpFoo Regexp regexpFoo.Regexp, _ = regexp.Compile("^(?:^(foo1|foo2|baz)$)$") var expectedConf = Config{ Global: &GlobalConfig{ HTTPConfig: &commoncfg.HTTPClientConfig{}, ResolveTimeout: model.Duration(5 * time.Minute), SMTPSmarthost: "localhost:25", SMTPFrom: "alertmanager@example.org", HipchatAuthToken: "mysecret", HipchatAPIURL: mustParseURL("https://hipchat.foobar.org/"), SlackAPIURL: (*SecretURL)(mustParseURL("http://slack.example.com/")), SMTPRequireTLS: true, PagerdutyURL: mustParseURL("https://events.pagerduty.com/v2/enqueue"), OpsGenieAPIURL: mustParseURL("https://api.opsgenie.com/"), WeChatAPIURL: mustParseURL("https://qyapi.weixin.qq.com/cgi-bin/"), VictorOpsAPIURL: mustParseURL("https://alert.victorops.com/integrations/generic/20131114/alert/"), }, Templates: []string{ "/etc/alertmanager/template/*.tmpl", }, Route: &Route{ Receiver: "team-X-mails", GroupBy: []model.LabelName{ "alertname", "cluster", "service", }, Routes: []*Route{ { Receiver: "team-X-mails", MatchRE: map[string]Regexp{ "service": regexpFoo, }, }, }, }, Receivers: []*Receiver{ { Name: "team-X-mails", EmailConfigs: []*EmailConfig{ { To: "team-X+alerts@example.org", From: "alertmanager@example.org", Smarthost: "localhost:25", HTML: "{{ template \"email.default.html\" . }}", RequireTLS: &boolFoo, }, }, }, }, } config, _, err := LoadFile("testdata/conf.empty-fields.yml") if err != nil { t.Errorf("Error parsing %s: %s", "testdata/conf.empty-fields.yml", err) } configGot, err := yaml.Marshal(config) if err != nil { t.Fatal("YAML Marshaling failed:", err) } configExp, err := yaml.Marshal(expectedConf) if err != nil { t.Fatalf("%s", err) } if !reflect.DeepEqual(configGot, configExp) { t.Fatalf("%s: unexpected config result: \n\n%s\n expected\n\n%s", "testdata/conf.empty-fields.yml", configGot, configExp) } } func TestSMTPHello(t *testing.T) { c, _, err := LoadFile("testdata/conf.good.yml") if err != nil { t.Errorf("Error parsing %s: %s", "testdata/conf.good.yml", err) } const refValue = "host.example.org" var hostName = c.Global.SMTPHello if hostName != refValue { t.Errorf("Invalid SMTP Hello hostname: %s\nExpected: %s", hostName, refValue) } } func TestVictorOpsDefaultAPIKey(t *testing.T) { conf, _, err := LoadFile("testdata/conf.victorops-default-apikey.yml") if err != nil { t.Errorf("Error parsing %s: %s", "testdata/conf.victorops-default-apikey.yml", err) } var defaultKey = conf.Global.VictorOpsAPIKey if defaultKey != conf.Receivers[0].VictorOpsConfigs[0].APIKey { t.Errorf("Invalid victorops key: %s\nExpected: %s", conf.Receivers[0].VictorOpsConfigs[0].APIKey, defaultKey) } if defaultKey == conf.Receivers[1].VictorOpsConfigs[0].APIKey { t.Errorf("Invalid victorops key: %s\nExpected: %s", conf.Receivers[0].VictorOpsConfigs[0].APIKey, "qwe456") } } func TestVictorOpsNoAPIKey(t *testing.T) { _, _, err := LoadFile("testdata/conf.victorops-no-apikey.yml") if err == nil { t.Errorf("Expected an error parsing %s: %s", "testdata/conf.victorops-no-apikey.yml", err) } if err.Error() != "no global VictorOps API Key set" { t.Errorf("Expected: %s\nGot: %s", "no global VictorOps API Key set", err.Error()) } } func TestOpsGenieDefaultAPIKey(t *testing.T) { conf, _, err := LoadFile("testdata/conf.opsgenie-default-apikey.yml") if err != nil { t.Errorf("Error parsing %s: %s", "testdata/conf.opsgenie-default-apikey.yml", err) } var defaultKey = conf.Global.OpsGenieAPIKey if defaultKey != conf.Receivers[0].OpsGenieConfigs[0].APIKey { t.Errorf("Invalid OpsGenie key: %s\nExpected: %s", conf.Receivers[0].OpsGenieConfigs[0].APIKey, defaultKey) } if defaultKey == conf.Receivers[1].OpsGenieConfigs[0].APIKey { t.Errorf("Invalid OpsGenie key: %s\nExpected: %s", conf.Receivers[0].OpsGenieConfigs[0].APIKey, "qwe456") } } func TestOpsGenieNoAPIKey(t *testing.T) { _, _, err := LoadFile("testdata/conf.opsgenie-no-apikey.yml") if err == nil { t.Errorf("Expected an error parsing %s: %s", "testdata/conf.opsgenie-no-apikey.yml", err) } if err.Error() != "no global OpsGenie API Key set" { t.Errorf("Expected: %s\nGot: %s", "no global OpsGenie API Key set", err.Error()) } } prometheus-alertmanager-0.15.3+ds/config/notifiers.go000066400000000000000000000466711341674552200226620ustar00rootroot00000000000000// Copyright 2015 Prometheus Team // 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 config import ( "fmt" "strings" "time" commoncfg "github.com/prometheus/common/config" ) var ( // DefaultWebhookConfig defines default values for Webhook configurations. DefaultWebhookConfig = WebhookConfig{ NotifierConfig: NotifierConfig{ VSendResolved: true, }, } // DefaultEmailConfig defines default values for Email configurations. DefaultEmailConfig = EmailConfig{ NotifierConfig: NotifierConfig{ VSendResolved: false, }, HTML: `{{ template "email.default.html" . }}`, Text: ``, } // DefaultEmailSubject defines the default Subject header of an Email. DefaultEmailSubject = `{{ template "email.default.subject" . }}` // DefaultPagerdutyDetails defines the default values for PagerDuty details. DefaultPagerdutyDetails = map[string]string{ "firing": `{{ template "pagerduty.default.instances" .Alerts.Firing }}`, "resolved": `{{ template "pagerduty.default.instances" .Alerts.Resolved }}`, "num_firing": `{{ .Alerts.Firing | len }}`, "num_resolved": `{{ .Alerts.Resolved | len }}`, } // DefaultPagerdutyConfig defines default values for PagerDuty configurations. DefaultPagerdutyConfig = PagerdutyConfig{ NotifierConfig: NotifierConfig{ VSendResolved: true, }, Description: `{{ template "pagerduty.default.description" .}}`, Client: `{{ template "pagerduty.default.client" . }}`, ClientURL: `{{ template "pagerduty.default.clientURL" . }}`, } // DefaultSlackConfig defines default values for Slack configurations. DefaultSlackConfig = SlackConfig{ NotifierConfig: NotifierConfig{ VSendResolved: false, }, Color: `{{ if eq .Status "firing" }}danger{{ else }}good{{ end }}`, Username: `{{ template "slack.default.username" . }}`, Title: `{{ template "slack.default.title" . }}`, TitleLink: `{{ template "slack.default.titlelink" . }}`, IconEmoji: `{{ template "slack.default.iconemoji" . }}`, IconURL: `{{ template "slack.default.iconurl" . }}`, Pretext: `{{ template "slack.default.pretext" . }}`, Text: `{{ template "slack.default.text" . }}`, Fallback: `{{ template "slack.default.fallback" . }}`, Footer: `{{ template "slack.default.footer" . }}`, } // DefaultHipchatConfig defines default values for Hipchat configurations. DefaultHipchatConfig = HipchatConfig{ NotifierConfig: NotifierConfig{ VSendResolved: false, }, Color: `{{ if eq .Status "firing" }}red{{ else }}green{{ end }}`, From: `{{ template "hipchat.default.from" . }}`, Notify: false, Message: `{{ template "hipchat.default.message" . }}`, MessageFormat: `text`, } // DefaultOpsGenieConfig defines default values for OpsGenie configurations. DefaultOpsGenieConfig = OpsGenieConfig{ NotifierConfig: NotifierConfig{ VSendResolved: true, }, Message: `{{ template "opsgenie.default.message" . }}`, Description: `{{ template "opsgenie.default.description" . }}`, Source: `{{ template "opsgenie.default.source" . }}`, // TODO: Add a details field with all the alerts. } // DefaultWechatConfig defines default values for wechat configurations. DefaultWechatConfig = WechatConfig{ NotifierConfig: NotifierConfig{ VSendResolved: false, }, Message: `{{ template "wechat.default.message" . }}`, APISecret: `{{ template "wechat.default.api_secret" . }}`, ToUser: `{{ template "wechat.default.to_user" . }}`, ToParty: `{{ template "wechat.default.to_party" . }}`, ToTag: `{{ template "wechat.default.to_tag" . }}`, AgentID: `{{ template "wechat.default.agent_id" . }}`, } // DefaultVictorOpsConfig defines default values for VictorOps configurations. DefaultVictorOpsConfig = VictorOpsConfig{ NotifierConfig: NotifierConfig{ VSendResolved: true, }, MessageType: `CRITICAL`, StateMessage: `{{ template "victorops.default.state_message" . }}`, EntityDisplayName: `{{ template "victorops.default.entity_display_name" . }}`, MonitoringTool: `{{ template "victorops.default.monitoring_tool" . }}`, } // DefaultPushoverConfig defines default values for Pushover configurations. DefaultPushoverConfig = PushoverConfig{ NotifierConfig: NotifierConfig{ VSendResolved: true, }, Title: `{{ template "pushover.default.title" . }}`, Message: `{{ template "pushover.default.message" . }}`, URL: `{{ template "pushover.default.url" . }}`, Priority: `{{ if eq .Status "firing" }}2{{ else }}0{{ end }}`, // emergency (firing) or normal Retry: duration(1 * time.Minute), Expire: duration(1 * time.Hour), } ) // NotifierConfig contains base options common across all notifier configurations. type NotifierConfig struct { VSendResolved bool `yaml:"send_resolved" json:"send_resolved"` } func (nc *NotifierConfig) SendResolved() bool { return nc.VSendResolved } // EmailConfig configures notifications via mail. type EmailConfig struct { NotifierConfig `yaml:",inline" json:",inline"` // Email address to notify. To string `yaml:"to,omitempty" json:"to,omitempty"` From string `yaml:"from,omitempty" json:"from,omitempty"` Hello string `yaml:"hello,omitempty" json:"hello,omitempty"` Smarthost string `yaml:"smarthost,omitempty" json:"smarthost,omitempty"` AuthUsername string `yaml:"auth_username,omitempty" json:"auth_username,omitempty"` AuthPassword Secret `yaml:"auth_password,omitempty" json:"auth_password,omitempty"` AuthSecret Secret `yaml:"auth_secret,omitempty" json:"auth_secret,omitempty"` AuthIdentity string `yaml:"auth_identity,omitempty" json:"auth_identity,omitempty"` Headers map[string]string `yaml:"headers,omitempty" json:"headers,omitempty"` HTML string `yaml:"html,omitempty" json:"html,omitempty"` Text string `yaml:"text,omitempty" json:"text,omitempty"` RequireTLS *bool `yaml:"require_tls,omitempty" json:"require_tls,omitempty"` } // UnmarshalYAML implements the yaml.Unmarshaler interface. func (c *EmailConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { *c = DefaultEmailConfig type plain EmailConfig if err := unmarshal((*plain)(c)); err != nil { return err } if c.To == "" { return fmt.Errorf("missing to address in email config") } // Header names are case-insensitive, check for collisions. normalizedHeaders := map[string]string{} for h, v := range c.Headers { normalized := strings.Title(h) if _, ok := normalizedHeaders[normalized]; ok { return fmt.Errorf("duplicate header %q in email config", normalized) } normalizedHeaders[normalized] = v } c.Headers = normalizedHeaders return nil } // PagerdutyConfig configures notifications via PagerDuty. type PagerdutyConfig struct { NotifierConfig `yaml:",inline" json:",inline"` HTTPConfig *commoncfg.HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"` ServiceKey Secret `yaml:"service_key,omitempty" json"service_key,omitempty"` RoutingKey Secret `yaml:"routing_key,omitempty" json:"routing_key,omitempty"` URL *URL `yaml:"url,omitempty" json:"url,omitempty"` Client string `yaml:"client,omitempty" json:"client,omitempty"` ClientURL string `yaml:"client_url,omitempty" json:"client_url,omitempty"` Description string `yaml:"description,omitempty" json:"description,omitempty"` Details map[string]string `yaml:"details,omitempty" json:"details,omitempty"` Severity string `yaml:"severity,omitempty" json:"severity,omitempty"` Class string `yaml:"class,omitempty" json:"class,omitempty"` Component string `yaml:"component,omitempty" json:"component,omitempty"` Group string `yaml:"group,omitempty" json:"group,omitempty"` } // UnmarshalYAML implements the yaml.Unmarshaler interface. func (c *PagerdutyConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { *c = DefaultPagerdutyConfig type plain PagerdutyConfig if err := unmarshal((*plain)(c)); err != nil { return err } if c.RoutingKey == "" && c.ServiceKey == "" { return fmt.Errorf("missing service or routing key in PagerDuty config") } if c.Details == nil { c.Details = make(map[string]string) } for k, v := range DefaultPagerdutyDetails { if _, ok := c.Details[k]; !ok { c.Details[k] = v } } return nil } // SlackAction configures a single Slack action that is sent with each notification. // Each action must contain a type, text, and url. // See https://api.slack.com/docs/message-attachments#action_fields for more information. type SlackAction struct { Type string `yaml:"type,omitempty" json:"type,omitempty"` Text string `yaml:"text,omitempty" json:"text,omitempty"` URL string `yaml:"url,omitempty" json:"url,omitempty"` Style string `yaml:"style,omitempty" json:"style,omitempty"` } // UnmarshalYAML implements the yaml.Unmarshaler interface for SlackAction. func (c *SlackAction) UnmarshalYAML(unmarshal func(interface{}) error) error { type plain SlackAction if err := unmarshal((*plain)(c)); err != nil { return err } if c.Type == "" { return fmt.Errorf("missing type in Slack action configuration") } if c.Text == "" { return fmt.Errorf("missing value in Slack text configuration") } if c.URL == "" { return fmt.Errorf("missing value in Slack url configuration") } return nil } // SlackField configures a single Slack field that is sent with each notification. // Each field must contain a title, value, and optionally, a boolean value to indicate if the field // is short enough to be displayed next to other fields designated as short. // See https://api.slack.com/docs/message-attachments#fields for more information. type SlackField struct { Title string `yaml:"title,omitempty" json:"title,omitempty"` Value string `yaml:"value,omitempty" json:"value,omitempty"` Short *bool `yaml:"short,omitempty" json:"short,omitempty"` } // UnmarshalYAML implements the yaml.Unmarshaler interface for SlackField. func (c *SlackField) UnmarshalYAML(unmarshal func(interface{}) error) error { type plain SlackField if err := unmarshal((*plain)(c)); err != nil { return err } if c.Title == "" { return fmt.Errorf("missing title in Slack field configuration") } if c.Value == "" { return fmt.Errorf("missing value in Slack field configuration") } return nil } // SlackConfig configures notifications via Slack. type SlackConfig struct { NotifierConfig `yaml:",inline" json:",inline"` HTTPConfig *commoncfg.HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"` APIURL *SecretURL `yaml:"api_url,omitempty" json:"api_url,omitempty"` // Slack channel override, (like #other-channel or @username). Channel string `yaml:"channel,omitempty" json:"channel,omitempty"` Username string `yaml:"username,omitempty" json:"username,omitempty"` Color string `yaml:"color,omitempty" json:"color,omitempty"` Title string `yaml:"title,omitempty" json:"title,omitempty"` TitleLink string `yaml:"title_link,omitempty" json:"title_link,omitempty"` Pretext string `yaml:"pretext,omitempty" json:"pretext,omitempty"` Text string `yaml:"text,omitempty" json:"text,omitempty"` Fields []*SlackField `yaml:"fields,omitempty" json:"fields,omitempty"` ShortFields bool `yaml:"short_fields,omitempty" json:"short_fields,omitempty"` Footer string `yaml:"footer,omitempty" json:"footer,omitempty"` Fallback string `yaml:"fallback,omitempty" json:"fallback,omitempty"` IconEmoji string `yaml:"icon_emoji,omitempty" json:"icon_emoji,omitempty"` IconURL string `yaml:"icon_url,omitempty" json:"icon_url,omitempty"` LinkNames bool `yaml:"link_names,omitempty" json:"link_names,omitempty"` Actions []*SlackAction `yaml:"actions,omitempty" json:"actions,omitempty"` } // UnmarshalYAML implements the yaml.Unmarshaler interface. func (c *SlackConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { *c = DefaultSlackConfig type plain SlackConfig return unmarshal((*plain)(c)) } // HipchatConfig configures notifications via Hipchat. type HipchatConfig struct { NotifierConfig `yaml:",inline" json:",inline"` HTTPConfig *commoncfg.HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"` APIURL *URL `yaml:"api_url,omitempty" json:"api_url,omitempty"` AuthToken Secret `yaml:"auth_token,omitempty" json:"auth_token,omitempty"` RoomID string `yaml:"room_id,omitempty" json:"room_id,omitempty"` From string `yaml:"from,omitempty" json:"from,omitempty"` Notify bool `yaml:"notify,omitempty" json:"notify,omitempty"` Message string `yaml:"message,omitempty" json:"message,omitempty"` MessageFormat string `yaml:"message_format,omitempty" json:"message_format,omitempty"` Color string `yaml:"color,omitempty" json:"color,omitempty"` } // UnmarshalYAML implements the yaml.Unmarshaler interface. func (c *HipchatConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { *c = DefaultHipchatConfig type plain HipchatConfig if err := unmarshal((*plain)(c)); err != nil { return err } if c.RoomID == "" { return fmt.Errorf("missing room id in Hipchat config") } return nil } // WebhookConfig configures notifications via a generic webhook. type WebhookConfig struct { NotifierConfig `yaml:",inline" json:",inline"` HTTPConfig *commoncfg.HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"` // URL to send POST request to. URL *URL `yaml:"url" json:"url"` } // UnmarshalYAML implements the yaml.Unmarshaler interface. func (c *WebhookConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { *c = DefaultWebhookConfig type plain WebhookConfig if err := unmarshal((*plain)(c)); err != nil { return err } if c.URL == nil { return fmt.Errorf("missing URL in webhook config") } if c.URL.Scheme != "https" && c.URL.Scheme != "http" { return fmt.Errorf("scheme required for webhook url") } return nil } // WechatConfig configures notifications via Wechat. type WechatConfig struct { NotifierConfig `yaml:",inline" json:",inline"` HTTPConfig *commoncfg.HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"` APISecret Secret `yaml:"api_secret,omitempty" json:"api_secret,omitempty"` CorpID string `yaml:"corp_id,omitempty" json:"corp_id,omitempty"` Message string `yaml:"message,omitempty" json:"message,omitempty"` APIURL *URL `yaml:"api_url,omitempty" json:"api_url,omitempty"` ToUser string `yaml:"to_user,omitempty" json:"to_user,omitempty"` ToParty string `yaml:"to_party,omitempty" json:"to_party,omitempty"` ToTag string `yaml:"to_tag,omitempty" json:"to_tag,omitempty"` AgentID string `yaml:"agent_id,omitempty" json:"agent_id,omitempty"` } // UnmarshalYAML implements the yaml.Unmarshaler interface. func (c *WechatConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { *c = DefaultWechatConfig type plain WechatConfig if err := unmarshal((*plain)(c)); err != nil { return err } if c.APISecret == "" { return fmt.Errorf("missing Wechat APISecret in Wechat config") } if c.CorpID == "" { return fmt.Errorf("missing Wechat CorpID in Wechat config") } return nil } // OpsGenieConfig configures notifications via OpsGenie. type OpsGenieConfig struct { NotifierConfig `yaml:",inline" json:",inline"` HTTPConfig *commoncfg.HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"` APIKey Secret `yaml:"api_key,omitempty" json:"api_key,omitempty"` APIURL *URL `yaml:"api_url,omitempty" json:"api_url,omitempty"` Message string `yaml:"message,omitempty" json:"message,omitempty"` Description string `yaml:"description,omitempty" json:"description,omitempty"` Source string `yaml:"source,omitempty" json:"source,omitempty"` Details map[string]string `yaml:"details,omitempty" json:"details,omitempty"` Teams string `yaml:"teams,omitempty" json:"teams,omitempty"` Tags string `yaml:"tags,omitempty" json:"tags,omitempty"` Note string `yaml:"note,omitempty" json:"note,omitempty"` Priority string `yaml:"priority,omitempty" json:"priority,omitempty"` } // UnmarshalYAML implements the yaml.Unmarshaler interface. func (c *OpsGenieConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { *c = DefaultOpsGenieConfig type plain OpsGenieConfig return unmarshal((*plain)(c)) } // VictorOpsConfig configures notifications via VictorOps. type VictorOpsConfig struct { NotifierConfig `yaml:",inline" json:",inline"` HTTPConfig *commoncfg.HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"` APIKey Secret `yaml:"api_key" json:"api_key"` APIURL *URL `yaml:"api_url" json:"api_url"` RoutingKey string `yaml:"routing_key" json:"routing_key"` MessageType string `yaml:"message_type" json:"message_type"` StateMessage string `yaml:"state_message" json:"state_message"` EntityDisplayName string `yaml:"entity_display_name" json:"entity_display_name"` MonitoringTool string `yaml:"monitoring_tool" json:"monitoring_tool"` } // UnmarshalYAML implements the yaml.Unmarshaler interface. func (c *VictorOpsConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { *c = DefaultVictorOpsConfig type plain VictorOpsConfig if err := unmarshal((*plain)(c)); err != nil { return err } if c.RoutingKey == "" { return fmt.Errorf("missing Routing key in VictorOps config") } return nil } type duration time.Duration func (d *duration) UnmarshalText(text []byte) error { parsed, err := time.ParseDuration(string(text)) if err == nil { *d = duration(parsed) } return err } func (d duration) MarshalText() ([]byte, error) { return []byte(time.Duration(d).String()), nil } type PushoverConfig struct { NotifierConfig `yaml:",inline" json:",inline"` HTTPConfig *commoncfg.HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"` UserKey Secret `yaml:"user_key,omitempty" json:"user_key,omitempty"` Token Secret `yaml:"token,omitempty" json:"token,omitempty"` Title string `yaml:"title,omitempty" json:"title,omitempty"` Message string `yaml:"message,omitempty" json:"message,omitempty"` URL string `yaml:"url,omitempty" json:"url,omitempty"` Priority string `yaml:"priority,omitempty" json:"priority,omitempty"` Retry duration `yaml:"retry,omitempty" json:"retry,omitempty"` Expire duration `yaml:"expire,omitempty" json:"expire,omitempty"` } // UnmarshalYAML implements the yaml.Unmarshaler interface. func (c *PushoverConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { *c = DefaultPushoverConfig type plain PushoverConfig if err := unmarshal((*plain)(c)); err != nil { return err } if c.UserKey == "" { return fmt.Errorf("missing user key in Pushover config") } if c.Token == "" { return fmt.Errorf("missing token in Pushover config") } return nil } prometheus-alertmanager-0.15.3+ds/config/notifiers_test.go000066400000000000000000000225441341674552200237120ustar00rootroot00000000000000// Copyright 2018 Prometheus Team // 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 config import ( "strings" "testing" "gopkg.in/yaml.v2" ) func TestEmailToIsPresent(t *testing.T) { in := ` to: '' ` var cfg EmailConfig err := yaml.UnmarshalStrict([]byte(in), &cfg) expected := "missing to address in email config" if err == nil { t.Fatalf("no error returned, expected:\n%v", expected) } if err.Error() != expected { t.Errorf("\nexpected:\n%v\ngot:\n%v", expected, err.Error()) } } func TestEmailHeadersCollision(t *testing.T) { in := ` to: 'to@email.com' headers: Subject: 'Alert' subject: 'New Alert' ` var cfg EmailConfig err := yaml.UnmarshalStrict([]byte(in), &cfg) expected := "duplicate header \"Subject\" in email config" if err == nil { t.Fatalf("no error returned, expected:\n%v", expected) } if err.Error() != expected { t.Errorf("\nexpected:\n%v\ngot:\n%v", expected, err.Error()) } } func TestPagerdutyRoutingKeyIsPresent(t *testing.T) { in := ` routing_key: '' ` var cfg PagerdutyConfig err := yaml.UnmarshalStrict([]byte(in), &cfg) expected := "missing service or routing key in PagerDuty config" if err == nil { t.Fatalf("no error returned, expected:\n%v", expected) } if err.Error() != expected { t.Errorf("\nexpected:\n%v\ngot:\n%v", expected, err.Error()) } } func TestPagerdutyServiceKeyIsPresent(t *testing.T) { in := ` service_key: '' ` var cfg PagerdutyConfig err := yaml.UnmarshalStrict([]byte(in), &cfg) expected := "missing service or routing key in PagerDuty config" if err == nil { t.Fatalf("no error returned, expected:\n%v", expected) } if err.Error() != expected { t.Errorf("\nexpected:\n%v\ngot:\n%v", expected, err.Error()) } } func TestPagerdutyDetails(t *testing.T) { var tests = []struct { in string checkFn func(map[string]string) }{ { in: ` routing_key: 'xyz' `, checkFn: func(d map[string]string) { if len(d) != 4 { t.Errorf("expected 4 items, got: %d", len(d)) } }, }, { in: ` routing_key: 'xyz' details: key1: val1 `, checkFn: func(d map[string]string) { if len(d) != 5 { t.Errorf("expected 5 items, got: %d", len(d)) } }, }, { in: ` routing_key: 'xyz' details: key1: val1 key2: val2 firing: firing `, checkFn: func(d map[string]string) { if len(d) != 6 { t.Errorf("expected 6 items, got: %d", len(d)) } }, }, } for _, tc := range tests { var cfg PagerdutyConfig err := yaml.UnmarshalStrict([]byte(tc.in), &cfg) if err != nil { t.Errorf("expected no error, got:%v", err) } if tc.checkFn != nil { tc.checkFn(cfg.Details) } } } func TestHipchatRoomIDIsPresent(t *testing.T) { in := ` room_id: '' ` var cfg HipchatConfig err := yaml.UnmarshalStrict([]byte(in), &cfg) expected := "missing room id in Hipchat config" if err == nil { t.Fatalf("no error returned, expected:\n%v", expected) } if err.Error() != expected { t.Errorf("\nexpected:\n%v\ngot:\n%v", expected, err.Error()) } } func TestWebhookURLIsPresent(t *testing.T) { in := `{}` var cfg WebhookConfig err := yaml.UnmarshalStrict([]byte(in), &cfg) expected := "missing URL in webhook config" if err == nil { t.Fatalf("no error returned, expected:\n%v", expected) } if err.Error() != expected { t.Errorf("\nexpected:\n%v\ngot:\n%v", expected, err.Error()) } } func TestWebhookHttpConfigIsValid(t *testing.T) { in := ` url: 'http://example.com' http_config: bearer_token: foo bearer_token_file: /tmp/bar ` var cfg WebhookConfig err := yaml.UnmarshalStrict([]byte(in), &cfg) expected := "at most one of bearer_token & bearer_token_file must be configured" if err == nil { t.Fatalf("no error returned, expected:\n%v", expected) } if err.Error() != expected { t.Errorf("\nexpected:\n%v\ngot:\n%v", expected, err.Error()) } } func TestWebhookHttpConfigIsOptional(t *testing.T) { in := ` url: 'http://example.com' ` var cfg WebhookConfig err := yaml.UnmarshalStrict([]byte(in), &cfg) if err != nil { t.Fatalf("no error expected, returned:\n%v", err.Error()) } } func TestWebhookPasswordIsObsfucated(t *testing.T) { in := ` url: 'http://example.com' http_config: basic_auth: username: foo password: supersecret ` var cfg WebhookConfig err := yaml.UnmarshalStrict([]byte(in), &cfg) if err != nil { t.Fatalf("no error expected, returned:\n%v", err.Error()) } ycfg, err := yaml.Marshal(cfg) if err != nil { t.Fatalf("no error expected, returned:\n%v", err.Error()) } if strings.Contains(string(ycfg), "supersecret") { t.Errorf("Found password in the YAML cfg: %s\n", ycfg) } } func TestWechatAPIKeyIsPresent(t *testing.T) { in := ` api_secret: '' ` var cfg WechatConfig err := yaml.UnmarshalStrict([]byte(in), &cfg) expected := "missing Wechat APISecret in Wechat config" if err == nil { t.Fatalf("no error returned, expected:\n%v", expected) } if err.Error() != expected { t.Errorf("\nexpected:\n%v\ngot:\n%v", expected, err.Error()) } } func TestWechatCorpIDIsPresent(t *testing.T) { in := ` api_secret: 'api_secret' corp_id: '' ` var cfg WechatConfig err := yaml.UnmarshalStrict([]byte(in), &cfg) expected := "missing Wechat CorpID in Wechat config" if err == nil { t.Fatalf("no error returned, expected:\n%v", expected) } if err.Error() != expected { t.Errorf("\nexpected:\n%v\ngot:\n%v", expected, err.Error()) } } func TestVictorOpsRoutingKeyIsPresent(t *testing.T) { in := ` routing_key: '' ` var cfg VictorOpsConfig err := yaml.UnmarshalStrict([]byte(in), &cfg) expected := "missing Routing key in VictorOps config" if err == nil { t.Fatalf("no error returned, expected:\n%v", expected) } if err.Error() != expected { t.Errorf("\nexpected:\n%v\ngot:\n%v", expected, err.Error()) } } func TestPushoverUserKeyIsPresent(t *testing.T) { in := ` user_key: '' ` var cfg PushoverConfig err := yaml.UnmarshalStrict([]byte(in), &cfg) expected := "missing user key in Pushover config" if err == nil { t.Fatalf("no error returned, expected:\n%v", expected) } if err.Error() != expected { t.Errorf("\nexpected:\n%v\ngot:\n%v", expected, err.Error()) } } func TestPushoverTokenIsPresent(t *testing.T) { in := ` user_key: '' token: '' ` var cfg PushoverConfig err := yaml.UnmarshalStrict([]byte(in), &cfg) expected := "missing token in Pushover config" if err == nil { t.Fatalf("no error returned, expected:\n%v", expected) } if err.Error() != expected { t.Errorf("\nexpected:\n%v\ngot:\n%v", expected, err.Error()) } } func TestSlackFieldConfigValidation(t *testing.T) { var tests = []struct { in string expected string }{ { in: ` fields: - title: first value: hello - title: second `, expected: "missing value in Slack field configuration", }, { in: ` fields: - title: first value: hello short: true - value: world short: true `, expected: "missing title in Slack field configuration", }, { in: ` fields: - title: first value: hello short: true - title: second value: world `, expected: "", }, } for _, rt := range tests { var cfg SlackConfig err := yaml.UnmarshalStrict([]byte(rt.in), &cfg) // Check if an error occurred when it was NOT expected to. if rt.expected == "" && err != nil { t.Fatalf("\nerror returned when none expected, error:\n%v", err) } // Check that an error occurred if one was expected to. if rt.expected != "" && err == nil { t.Fatalf("\nno error returned, expected:\n%v", rt.expected) } // Check that the error that occurred was what was expected. if err != nil && err.Error() != rt.expected { t.Errorf("\nexpected:\n%v\ngot:\n%v", rt.expected, err.Error()) } } } func TestSlackFieldConfigUnmarshalling(t *testing.T) { in := ` fields: - title: first value: hello short: true - title: second value: world - title: third value: slack field test short: false ` expected := []*SlackField{ &SlackField{ Title: "first", Value: "hello", Short: newBoolPointer(true), }, &SlackField{ Title: "second", Value: "world", Short: nil, }, &SlackField{ Title: "third", Value: "slack field test", Short: newBoolPointer(false), }, } var cfg SlackConfig err := yaml.UnmarshalStrict([]byte(in), &cfg) if err != nil { t.Fatalf("\nerror returned when none expected, error:\n%v", err) } for index, field := range cfg.Fields { exp := expected[index] if field.Title != exp.Title { t.Errorf("\nexpected:\n%v\ngot:\n%v", exp.Title, field.Title) } if field.Value != exp.Value { t.Errorf("\nexpected:\n%v\ngot:\n%v", exp.Value, field.Value) } if exp.Short == nil && field.Short != nil { t.Errorf("\nexpected:\n%v\ngot:\n%v", exp.Short, *field.Short) } if exp.Short != nil && field.Short == nil { t.Errorf("\nexpected:\n%v\ngot:\n%v", *exp.Short, field.Short) } if exp.Short != nil && *exp.Short != *field.Short { t.Errorf("\nexpected:\n%v\ngot:\n%v", *exp.Short, *field.Short) } } } func newBoolPointer(b bool) *bool { return &b } prometheus-alertmanager-0.15.3+ds/config/testdata/000077500000000000000000000000001341674552200221245ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/config/testdata/conf.empty-fields.yml000066400000000000000000000010641341674552200261760ustar00rootroot00000000000000global: smtp_smarthost: 'localhost:25' smtp_from: 'alertmanager@example.org' smtp_auth_username: '' smtp_auth_password: '' smtp_hello: '' hipchat_auth_token: 'mysecret' hipchat_api_url: 'https://hipchat.foobar.org/' slack_api_url: 'mysecret' templates: - '/etc/alertmanager/template/*.tmpl' route: group_by: ['alertname', 'cluster', 'service'] receiver: team-X-mails routes: - match_re: service: ^(foo1|foo2|baz)$ receiver: team-X-mails receivers: - name: 'team-X-mails' email_configs: - to: 'team-X+alerts@example.org' prometheus-alertmanager-0.15.3+ds/config/testdata/conf.good.yml000066400000000000000000000075211341674552200245300ustar00rootroot00000000000000global: # The smarthost and SMTP sender used for mail notifications. smtp_smarthost: 'localhost:25' smtp_from: 'alertmanager@example.org' smtp_auth_username: 'alertmanager' smtp_auth_password: "multiline\nmysecret" smtp_hello: "host.example.org" # The auth token for Hipchat. hipchat_auth_token: "mysecret" # Alternative host for Hipchat. hipchat_api_url: 'https://hipchat.foobar.org/' slack_api_url: "http://mysecret.example.com/" # The directory from which notification templates are read. templates: - '/etc/alertmanager/template/*.tmpl' # The root route on which each incoming alert enters. route: # The labels by which incoming alerts are grouped together. For example, # multiple alerts coming in for cluster=A and alertname=LatencyHigh would # be batched into a single group. group_by: ['alertname', 'cluster', 'service'] # When a new group of alerts is created by an incoming alert, wait at # least 'group_wait' to send the initial notification. # This way ensures that you get multiple alerts for the same group that start # firing shortly after another are batched together on the first # notification. group_wait: 30s # When the first notification was sent, wait 'group_interval' to send a batch # of new alerts that started firing for that group. group_interval: 5m # If an alert has successfully been sent, wait 'repeat_interval' to # resend them. repeat_interval: 3h # A default receiver receiver: team-X-mails # All the above attributes are inherited by all child routes and can # overwritten on each. # The child route trees. routes: # This routes performs a regular expression match on alert labels to # catch alerts that are related to a list of services. - match_re: service: ^(foo1|foo2|baz)$ receiver: team-X-mails # The service has a sub-route for critical alerts, any alerts # that do not match, i.e. severity != critical, fall-back to the # parent node and are sent to 'team-X-mails' routes: - match: severity: critical receiver: team-X-pager - match: service: files receiver: team-Y-mails routes: - match: severity: critical receiver: team-Y-pager # This route handles all alerts coming from a database service. If there's # no team to handle it, it defaults to the DB team. - match: service: database receiver: team-DB-pager # Also group alerts by affected database. group_by: [alertname, cluster, database] routes: - match: owner2: team-X receiver: team-X-pager continue: true - match: owner: team-Y receiver: team-Y-pager # continue: true # Inhibition rules allow to mute a set of alerts given that another alert is # firing. # We use this to mute any warning-level notifications if the same alert is # already critical. inhibit_rules: - source_match: severity: 'critical' target_match: severity: 'warning' # Apply inhibition if the alertname is the same. equal: ['alertname', 'cluster', 'service'] receivers: - name: 'team-X-mails' email_configs: - to: 'team-X+alerts@example.org' - name: 'team-X-pager' email_configs: - to: 'team-X+alerts-critical@example.org' pagerduty_configs: - routing_key: "mysecret" - name: 'team-Y-mails' email_configs: - to: 'team-Y+alerts@example.org' - name: 'team-Y-pager' pagerduty_configs: - routing_key: "mysecret" - name: 'team-DB-pager' pagerduty_configs: - routing_key: "mysecret" - name: 'team-X-hipchat' hipchat_configs: - auth_token: "mysecret" room_id: 85 message_format: html notify: true - name: victorOps-receiver victorops_configs: - api_key: mysecret routing_key: Sample_route - name: opsGenie-receiver opsgenie_configs: - api_key: mysecret - name: pushover-receiver pushover_configs: - token: mysecret user_key: key prometheus-alertmanager-0.15.3+ds/config/testdata/conf.opsgenie-default-apikey.yml000066400000000000000000000006361341674552200303130ustar00rootroot00000000000000global: opsgenie_api_key: asd132 route: group_by: ['alertname', 'cluster', 'service'] group_wait: 30s group_interval: 5m repeat_interval: 3h receiver: team-Y-opsgenie routes: - match: service: foo receiver: team-X-opsgenie receivers: - name: 'team-X-opsgenie' opsgenie_configs: - teams: 'team-X' - name: 'team-Y-opsgenie' opsgenie_configs: - teams: 'team-Y' api_key: qwe456 prometheus-alertmanager-0.15.3+ds/config/testdata/conf.opsgenie-no-apikey.yml000066400000000000000000000004441341674552200273000ustar00rootroot00000000000000route: group_by: ['alertname', 'cluster', 'service'] group_wait: 30s group_interval: 5m repeat_interval: 3h receiver: team-X-opsgenie routes: - match: service: foo receiver: team-X-opsgenie receivers: - name: 'team-X-opsgenie' opsgenie_configs: - teams: 'team-X' prometheus-alertmanager-0.15.3+ds/config/testdata/conf.victorops-default-apikey.yml000066400000000000000000000006611341674552200305300ustar00rootroot00000000000000global: victorops_api_key: asd132 route: group_by: ['alertname', 'cluster', 'service'] group_wait: 30s group_interval: 5m repeat_interval: 3h receiver: team-Y-victorops routes: - match: service: foo receiver: team-X-victorops receivers: - name: 'team-X-victorops' victorops_configs: - routing_key: 'team-X' - name: 'team-Y-victorops' victorops_configs: - routing_key: 'team-Y' api_key: qwe456 prometheus-alertmanager-0.15.3+ds/config/testdata/conf.victorops-no-apikey.yml000066400000000000000000000004561341674552200275220ustar00rootroot00000000000000route: group_by: ['alertname', 'cluster', 'service'] group_wait: 30s group_interval: 5m repeat_interval: 3h receiver: team-X-victorops routes: - match: service: foo receiver: team-X-victorops receivers: - name: 'team-X-victorops' victorops_configs: - routing_key: 'team-X' prometheus-alertmanager-0.15.3+ds/dispatch/000077500000000000000000000000001341674552200206455ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/dispatch/dispatch.go000066400000000000000000000257051341674552200230040ustar00rootroot00000000000000// Copyright 2018 Prometheus Team // 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 dispatch import ( "fmt" "sort" "sync" "time" "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" "github.com/prometheus/common/model" "github.com/prometheus/prometheus/pkg/labels" "golang.org/x/net/context" "github.com/prometheus/alertmanager/notify" "github.com/prometheus/alertmanager/provider" "github.com/prometheus/alertmanager/types" ) // Dispatcher sorts incoming alerts into aggregation groups and // assigns the correct notifiers to each. type Dispatcher struct { route *Route alerts provider.Alerts stage notify.Stage marker types.Marker timeout func(time.Duration) time.Duration aggrGroups map[*Route]map[model.Fingerprint]*aggrGroup mtx sync.RWMutex done chan struct{} ctx context.Context cancel func() logger log.Logger } // NewDispatcher returns a new Dispatcher. func NewDispatcher( ap provider.Alerts, r *Route, s notify.Stage, mk types.Marker, to func(time.Duration) time.Duration, l log.Logger, ) *Dispatcher { disp := &Dispatcher{ alerts: ap, stage: s, route: r, marker: mk, timeout: to, logger: log.With(l, "component", "dispatcher"), } return disp } // Run starts dispatching alerts incoming via the updates channel. func (d *Dispatcher) Run() { d.done = make(chan struct{}) d.mtx.Lock() d.aggrGroups = map[*Route]map[model.Fingerprint]*aggrGroup{} d.mtx.Unlock() d.ctx, d.cancel = context.WithCancel(context.Background()) d.run(d.alerts.Subscribe()) close(d.done) } // AlertBlock contains a list of alerts associated with a set of // routing options. type AlertBlock struct { RouteOpts *RouteOpts `json:"routeOpts"` Alerts []*APIAlert `json:"alerts"` } // APIAlert is the API representation of an alert, which is a regular alert // annotated with silencing and inhibition info. type APIAlert struct { *model.Alert Status types.AlertStatus `json:"status"` Receivers []string `json:"receivers"` Fingerprint string `json:"fingerprint"` } // AlertGroup is a list of alert blocks grouped by the same label set. type AlertGroup struct { Labels model.LabelSet `json:"labels"` GroupKey string `json:"groupKey"` Blocks []*AlertBlock `json:"blocks"` } // AlertOverview is a representation of all active alerts in the system. type AlertOverview []*AlertGroup func (ao AlertOverview) Swap(i, j int) { ao[i], ao[j] = ao[j], ao[i] } func (ao AlertOverview) Less(i, j int) bool { return ao[i].Labels.Before(ao[j].Labels) } func (ao AlertOverview) Len() int { return len(ao) } func matchesFilterLabels(a *APIAlert, matchers []*labels.Matcher) bool { for _, m := range matchers { if v, prs := a.Labels[model.LabelName(m.Name)]; !prs || !m.Matches(string(v)) { return false } } return true } // Groups populates an AlertOverview from the dispatcher's internal state. func (d *Dispatcher) Groups(matchers []*labels.Matcher) AlertOverview { overview := AlertOverview{} d.mtx.RLock() defer d.mtx.RUnlock() seen := map[model.Fingerprint]*AlertGroup{} for route, ags := range d.aggrGroups { for _, ag := range ags { alertGroup, ok := seen[ag.fingerprint()] if !ok { alertGroup = &AlertGroup{Labels: ag.labels} alertGroup.GroupKey = ag.GroupKey() seen[ag.fingerprint()] = alertGroup } now := time.Now() var apiAlerts []*APIAlert for _, a := range types.Alerts(ag.alertSlice()...) { if !a.EndsAt.IsZero() && a.EndsAt.Before(now) { continue } status := d.marker.Status(a.Fingerprint()) aa := &APIAlert{ Alert: a, Status: status, Fingerprint: a.Fingerprint().String(), } if !matchesFilterLabels(aa, matchers) { continue } apiAlerts = append(apiAlerts, aa) } if len(apiAlerts) == 0 { continue } alertGroup.Blocks = append(alertGroup.Blocks, &AlertBlock{ RouteOpts: &route.RouteOpts, Alerts: apiAlerts, }) overview = append(overview, alertGroup) } } sort.Sort(overview) return overview } func (d *Dispatcher) run(it provider.AlertIterator) { cleanup := time.NewTicker(30 * time.Second) defer cleanup.Stop() defer it.Close() for { select { case alert, ok := <-it.Next(): if !ok { // Iterator exhausted for some reason. if err := it.Err(); err != nil { level.Error(d.logger).Log("msg", "Error on alert update", "err", err) } return } level.Debug(d.logger).Log("msg", "Received alert", "alert", alert) // Log errors but keep trying. if err := it.Err(); err != nil { level.Error(d.logger).Log("msg", "Error on alert update", "err", err) continue } for _, r := range d.route.Match(alert.Labels) { d.processAlert(alert, r) } case <-cleanup.C: d.mtx.Lock() for _, groups := range d.aggrGroups { for _, ag := range groups { if ag.empty() { ag.stop() delete(groups, ag.fingerprint()) } } } d.mtx.Unlock() case <-d.ctx.Done(): return } } } // Stop the dispatcher. func (d *Dispatcher) Stop() { if d == nil || d.cancel == nil { return } d.cancel() d.cancel = nil <-d.done } // notifyFunc is a function that performs notifcation for the alert // with the given fingerprint. It aborts on context cancelation. // Returns false iff notifying failed. type notifyFunc func(context.Context, ...*types.Alert) bool // processAlert determines in which aggregation group the alert falls // and inserts it. func (d *Dispatcher) processAlert(alert *types.Alert, route *Route) { groupLabels := model.LabelSet{} for ln, lv := range alert.Labels { if _, ok := route.RouteOpts.GroupBy[ln]; ok { groupLabels[ln] = lv } } fp := groupLabels.Fingerprint() d.mtx.Lock() defer d.mtx.Unlock() group, ok := d.aggrGroups[route] if !ok { group = map[model.Fingerprint]*aggrGroup{} d.aggrGroups[route] = group } // If the group does not exist, create it. ag, ok := group[fp] if !ok { ag = newAggrGroup(d.ctx, groupLabels, route, d.timeout, d.logger) group[fp] = ag go ag.run(func(ctx context.Context, alerts ...*types.Alert) bool { _, _, err := d.stage.Exec(ctx, d.logger, alerts...) if err != nil { level.Error(d.logger).Log("msg", "Notify for alerts failed", "num_alerts", len(alerts), "err", err) } return err == nil }) } ag.insert(alert) } // aggrGroup aggregates alert fingerprints into groups to which a // common set of routing options applies. // It emits notifications in the specified intervals. type aggrGroup struct { labels model.LabelSet opts *RouteOpts logger log.Logger routeKey string ctx context.Context cancel func() done chan struct{} next *time.Timer timeout func(time.Duration) time.Duration mtx sync.RWMutex alerts map[model.Fingerprint]*types.Alert hasFlushed bool } // newAggrGroup returns a new aggregation group. func newAggrGroup(ctx context.Context, labels model.LabelSet, r *Route, to func(time.Duration) time.Duration, logger log.Logger) *aggrGroup { if to == nil { to = func(d time.Duration) time.Duration { return d } } ag := &aggrGroup{ labels: labels, routeKey: r.Key(), opts: &r.RouteOpts, timeout: to, alerts: map[model.Fingerprint]*types.Alert{}, } ag.ctx, ag.cancel = context.WithCancel(ctx) ag.logger = log.With(logger, "aggrGroup", ag) // Set an initial one-time wait before flushing // the first batch of notifications. ag.next = time.NewTimer(ag.opts.GroupWait) return ag } func (ag *aggrGroup) fingerprint() model.Fingerprint { return ag.labels.Fingerprint() } func (ag *aggrGroup) GroupKey() string { return fmt.Sprintf("%s:%s", ag.routeKey, ag.labels) } func (ag *aggrGroup) String() string { return ag.GroupKey() } func (ag *aggrGroup) alertSlice() []*types.Alert { ag.mtx.RLock() defer ag.mtx.RUnlock() var alerts []*types.Alert for _, a := range ag.alerts { alerts = append(alerts, a) } return alerts } func (ag *aggrGroup) run(nf notifyFunc) { ag.done = make(chan struct{}) defer close(ag.done) defer ag.next.Stop() for { select { case now := <-ag.next.C: // Give the notifications time until the next flush to // finish before terminating them. ctx, cancel := context.WithTimeout(ag.ctx, ag.timeout(ag.opts.GroupInterval)) // The now time we retrieve from the ticker is the only reliable // point of time reference for the subsequent notification pipeline. // Calculating the current time directly is prone to flaky behavior, // which usually only becomes apparent in tests. ctx = notify.WithNow(ctx, now) // Populate context with information needed along the pipeline. ctx = notify.WithGroupKey(ctx, ag.GroupKey()) ctx = notify.WithGroupLabels(ctx, ag.labels) ctx = notify.WithReceiverName(ctx, ag.opts.Receiver) ctx = notify.WithRepeatInterval(ctx, ag.opts.RepeatInterval) // Wait the configured interval before calling flush again. ag.mtx.Lock() ag.next.Reset(ag.opts.GroupInterval) ag.hasFlushed = true ag.mtx.Unlock() ag.flush(func(alerts ...*types.Alert) bool { return nf(ctx, alerts...) }) cancel() case <-ag.ctx.Done(): return } } } func (ag *aggrGroup) stop() { // Calling cancel will terminate all in-process notifications // and the run() loop. ag.cancel() <-ag.done } // insert inserts the alert into the aggregation group. func (ag *aggrGroup) insert(alert *types.Alert) { ag.mtx.Lock() defer ag.mtx.Unlock() ag.alerts[alert.Fingerprint()] = alert // Immediately trigger a flush if the wait duration for this // alert is already over. if !ag.hasFlushed && alert.StartsAt.Add(ag.opts.GroupWait).Before(time.Now()) { ag.next.Reset(0) } } func (ag *aggrGroup) empty() bool { ag.mtx.RLock() defer ag.mtx.RUnlock() return len(ag.alerts) == 0 } // flush sends notifications for all new alerts. func (ag *aggrGroup) flush(notify func(...*types.Alert) bool) { if ag.empty() { return } ag.mtx.Lock() var ( alerts = make(map[model.Fingerprint]*types.Alert, len(ag.alerts)) alertsSlice = make(types.AlertSlice, 0, len(ag.alerts)) ) for fp, alert := range ag.alerts { alerts[fp] = alert alertsSlice = append(alertsSlice, alert) } sort.Stable(alertsSlice) ag.mtx.Unlock() level.Debug(ag.logger).Log("msg", "Flushing", "alerts", fmt.Sprintf("%v", alertsSlice)) if notify(alertsSlice...) { ag.mtx.Lock() for fp, a := range alerts { // Only delete if the fingerprint has not been inserted // again since we notified about it. if a.Resolved() && ag.alerts[fp] == a { delete(ag.alerts, fp) } } ag.mtx.Unlock() } } prometheus-alertmanager-0.15.3+ds/dispatch/dispatch_test.go000066400000000000000000000165751341674552200240500ustar00rootroot00000000000000// Copyright 2018 Prometheus Team // 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 dispatch import ( "reflect" "sort" "sync" "testing" "time" "github.com/go-kit/kit/log" "github.com/prometheus/common/model" "github.com/prometheus/prometheus/pkg/labels" "golang.org/x/net/context" "github.com/prometheus/alertmanager/notify" "github.com/prometheus/alertmanager/types" ) func newAPIAlert(labels model.LabelSet) APIAlert { return APIAlert{ Alert: &model.Alert{ Labels: labels, StartsAt: time.Now().Add(1 * time.Minute), EndsAt: time.Now().Add(1 * time.Hour), }, } } func TestFilterLabels(t *testing.T) { var ( a1 = newAPIAlert(model.LabelSet{ "a": "v1", "b": "v2", "c": "v3", }) a2 = newAPIAlert(model.LabelSet{ "a": "v1", "b": "v2", "c": "v4", }) a3 = newAPIAlert(model.LabelSet{ "a": "v1", "b": "v2", "c": "v5", }) a4 = newAPIAlert(model.LabelSet{ "foo": "bar", "baz": "qux", }) alertsSlices = []struct { in, want []APIAlert }{ { in: []APIAlert{a1, a2, a3}, want: []APIAlert{a1, a2, a3}, }, { in: []APIAlert{a1, a4}, want: []APIAlert{a1}, }, { in: []APIAlert{a4}, want: []APIAlert{}, }, } ) matcher, err := labels.NewMatcher(labels.MatchRegexp, "c", "v.*") if err != nil { t.Fatalf("error making matcher: %v", err) } matcher2, err := labels.NewMatcher(labels.MatchEqual, "a", "v1") if err != nil { t.Fatalf("error making matcher: %v", err) } matchers := []*labels.Matcher{matcher, matcher2} for _, alerts := range alertsSlices { got := []APIAlert{} for _, a := range alerts.in { if matchesFilterLabels(&a, matchers) { got = append(got, a) } } if !reflect.DeepEqual(got, alerts.want) { t.Fatalf("error: returned alerts do not match:\ngot %v\nwant %v", got, alerts.want) } } } func TestAggrGroup(t *testing.T) { lset := model.LabelSet{ "a": "v1", "b": "v2", } opts := &RouteOpts{ Receiver: "n1", GroupBy: map[model.LabelName]struct{}{}, GroupWait: 1 * time.Second, GroupInterval: 300 * time.Millisecond, RepeatInterval: 1 * time.Hour, } route := &Route{ RouteOpts: *opts, } var ( a1 = &types.Alert{ Alert: model.Alert{ Labels: model.LabelSet{ "a": "v1", "b": "v2", "c": "v3", }, StartsAt: time.Now().Add(time.Minute), EndsAt: time.Now().Add(time.Hour), }, UpdatedAt: time.Now(), } a2 = &types.Alert{ Alert: model.Alert{ Labels: model.LabelSet{ "a": "v1", "b": "v2", "c": "v4", }, StartsAt: time.Now().Add(-time.Hour), EndsAt: time.Now().Add(2 * time.Hour), }, UpdatedAt: time.Now(), } a3 = &types.Alert{ Alert: model.Alert{ Labels: model.LabelSet{ "a": "v1", "b": "v2", "c": "v5", }, StartsAt: time.Now().Add(time.Minute), EndsAt: time.Now().Add(5 * time.Minute), }, UpdatedAt: time.Now(), } ) var ( last = time.Now() current = time.Now() lastCurMtx = &sync.Mutex{} alertsCh = make(chan types.AlertSlice) ) ntfy := func(ctx context.Context, alerts ...*types.Alert) bool { // Validate that the context is properly populated. if _, ok := notify.Now(ctx); !ok { t.Errorf("now missing") } if _, ok := notify.GroupKey(ctx); !ok { t.Errorf("group key missing") } if lbls, ok := notify.GroupLabels(ctx); !ok || !reflect.DeepEqual(lbls, lset) { t.Errorf("wrong group labels: %q", lbls) } if rcv, ok := notify.ReceiverName(ctx); !ok || rcv != opts.Receiver { t.Errorf("wrong receiver: %q", rcv) } if ri, ok := notify.RepeatInterval(ctx); !ok || ri != opts.RepeatInterval { t.Errorf("wrong repeat interval: %q", ri) } lastCurMtx.Lock() last = current current = time.Now() lastCurMtx.Unlock() alertsCh <- types.AlertSlice(alerts) return true } // Test regular situation where we wait for group_wait to send out alerts. ag := newAggrGroup(context.Background(), lset, route, nil, log.NewNopLogger()) go ag.run(ntfy) ag.insert(a1) select { case <-time.After(2 * opts.GroupWait): t.Fatalf("expected initial batch after group_wait") case batch := <-alertsCh: if s := time.Since(last); s < opts.GroupWait { t.Fatalf("received batch too early after %v", s) } exp := types.AlertSlice{a1} sort.Sort(batch) if !reflect.DeepEqual(batch, exp) { t.Fatalf("expected alerts %v but got %v", exp, batch) } } for i := 0; i < 3; i++ { // New alert should come in after group interval. ag.insert(a3) select { case <-time.After(2 * opts.GroupInterval): t.Fatalf("expected new batch after group interval but received none") case batch := <-alertsCh: if s := time.Since(last); s < opts.GroupInterval { t.Fatalf("received batch too early after %v", s) } exp := types.AlertSlice{a1, a3} sort.Sort(batch) if !reflect.DeepEqual(batch, exp) { t.Fatalf("expected alerts %v but got %v", exp, batch) } } } ag.stop() // Add an alert that started more than group_interval in the past. We expect // immediate flushing. // Finally, set all alerts to be resolved. After successful notify the aggregation group // should empty itself. ag = newAggrGroup(context.Background(), lset, route, nil, log.NewNopLogger()) go ag.run(ntfy) ag.insert(a1) ag.insert(a2) // a2 lies way in the past so the initial group_wait should be skipped. select { case <-time.After(opts.GroupWait / 2): t.Fatalf("expected immediate alert but received none") case batch := <-alertsCh: exp := types.AlertSlice{a1, a2} sort.Sort(batch) if !reflect.DeepEqual(batch, exp) { t.Fatalf("expected alerts %v but got %v", exp, batch) } } for i := 0; i < 3; i++ { // New alert should come in after group interval. ag.insert(a3) select { case <-time.After(2 * opts.GroupInterval): t.Fatalf("expected new batch after group interval but received none") case batch := <-alertsCh: lastCurMtx.Lock() s := time.Since(last) lastCurMtx.Unlock() if s < opts.GroupInterval { t.Fatalf("received batch too early after %v", s) } exp := types.AlertSlice{a1, a2, a3} sort.Sort(batch) if !reflect.DeepEqual(batch, exp) { t.Fatalf("expected alerts %v but got %v", exp, batch) } } } // Resolve all alerts, they should be removed after the next batch was sent. a1r, a2r, a3r := *a1, *a2, *a3 resolved := types.AlertSlice{&a1r, &a2r, &a3r} for _, a := range resolved { a.EndsAt = time.Now() ag.insert(a) } select { case <-time.After(2 * opts.GroupInterval): t.Fatalf("expected new batch after group interval but received none") case batch := <-alertsCh: if s := time.Since(last); s < opts.GroupInterval { t.Fatalf("received batch too early after %v", s) } sort.Sort(batch) if !reflect.DeepEqual(batch, resolved) { t.Fatalf("expected alerts %v but got %v", resolved, batch) } if !ag.empty() { t.Fatalf("Expected aggregation group to be empty after resolving alerts: %v", ag) } } ag.stop() } prometheus-alertmanager-0.15.3+ds/dispatch/route.go000066400000000000000000000117201341674552200223330ustar00rootroot00000000000000// Copyright 2015 Prometheus Team // 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 dispatch import ( "encoding/json" "fmt" "sort" "time" "github.com/prometheus/common/model" "github.com/prometheus/alertmanager/config" "github.com/prometheus/alertmanager/types" ) // DefaultRouteOpts are the defaulting routing options which apply // to the root route of a routing tree. var DefaultRouteOpts = RouteOpts{ GroupWait: 30 * time.Second, GroupInterval: 5 * time.Minute, RepeatInterval: 4 * time.Hour, GroupBy: map[model.LabelName]struct{}{}, } // A Route is a node that contains definitions of how to handle alerts. type Route struct { parent *Route // The configuration parameters for matches of this route. RouteOpts RouteOpts // Equality or regex matchers an alert has to fulfill to match // this route. Matchers types.Matchers // If true, an alert matches further routes on the same level. Continue bool // Children routes of this route. Routes []*Route } // NewRoute returns a new route. func NewRoute(cr *config.Route, parent *Route) *Route { // Create default and overwrite with configured settings. opts := DefaultRouteOpts if parent != nil { opts = parent.RouteOpts } if cr.Receiver != "" { opts.Receiver = cr.Receiver } if cr.GroupBy != nil { opts.GroupBy = map[model.LabelName]struct{}{} for _, ln := range cr.GroupBy { opts.GroupBy[ln] = struct{}{} } } if cr.GroupWait != nil { opts.GroupWait = time.Duration(*cr.GroupWait) } if cr.GroupInterval != nil { opts.GroupInterval = time.Duration(*cr.GroupInterval) } if cr.RepeatInterval != nil { opts.RepeatInterval = time.Duration(*cr.RepeatInterval) } // Build matchers. var matchers types.Matchers for ln, lv := range cr.Match { matchers = append(matchers, types.NewMatcher(model.LabelName(ln), lv)) } for ln, lv := range cr.MatchRE { matchers = append(matchers, types.NewRegexMatcher(model.LabelName(ln), lv.Regexp)) } sort.Sort(matchers) route := &Route{ parent: parent, RouteOpts: opts, Matchers: matchers, Continue: cr.Continue, } route.Routes = NewRoutes(cr.Routes, route) return route } // NewRoutes returns a slice of routes. func NewRoutes(croutes []*config.Route, parent *Route) []*Route { res := []*Route{} for _, cr := range croutes { res = append(res, NewRoute(cr, parent)) } return res } // Match does a depth-first left-to-right search through the route tree // and returns the matching routing nodes. func (r *Route) Match(lset model.LabelSet) []*Route { if !r.Matchers.Match(lset) { return nil } var all []*Route for _, cr := range r.Routes { matches := cr.Match(lset) all = append(all, matches...) if matches != nil && !cr.Continue { break } } // If no child nodes were matches, the current node itself is a match. if len(all) == 0 { all = append(all, r) } return all } // Key returns a key for the route. It does not uniquely identify a the route in general. func (r *Route) Key() string { b := make([]byte, 0, 1024) if r.parent != nil { b = append(b, r.parent.Key()...) b = append(b, '/') } return string(append(b, r.Matchers.String()...)) } // RouteOpts holds various routing options necessary for processing alerts // that match a given route. type RouteOpts struct { // The identifier of the associated notification configuration. Receiver string // What labels to group alerts by for notifications. GroupBy map[model.LabelName]struct{} // How long to wait to group matching alerts before sending // a notification. GroupWait time.Duration GroupInterval time.Duration RepeatInterval time.Duration } func (ro *RouteOpts) String() string { var labels []model.LabelName for ln := range ro.GroupBy { labels = append(labels, ln) } return fmt.Sprintf("", ro.Receiver, labels, ro.GroupWait, ro.GroupInterval) } // MarshalJSON returns a JSON representation of the routing options. func (ro *RouteOpts) MarshalJSON() ([]byte, error) { v := struct { Receiver string `json:"receiver"` GroupBy model.LabelNames `json:"groupBy"` GroupWait time.Duration `json:"groupWait"` GroupInterval time.Duration `json:"groupInterval"` RepeatInterval time.Duration `json:"repeatInterval"` }{ Receiver: ro.Receiver, GroupWait: ro.GroupWait, GroupInterval: ro.GroupInterval, RepeatInterval: ro.RepeatInterval, } for ln := range ro.GroupBy { v.GroupBy = append(v.GroupBy, ln) } return json.Marshal(&v) } prometheus-alertmanager-0.15.3+ds/dispatch/route_test.go000066400000000000000000000131341341674552200233730ustar00rootroot00000000000000// Copyright 2015 Prometheus Team // 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 dispatch import ( "reflect" "testing" "time" "github.com/prometheus/common/model" "gopkg.in/yaml.v2" "github.com/prometheus/alertmanager/config" ) func TestRouteMatch(t *testing.T) { in := ` receiver: 'notify-def' routes: - match: owner: 'team-A' receiver: 'notify-A' routes: - match: env: 'testing' receiver: 'notify-testing' group_by: [] - match: env: "production" receiver: 'notify-productionA' group_wait: 1m continue: true - match_re: env: "produ.*" job: ".*" receiver: 'notify-productionB' group_wait: 30s group_interval: 5m repeat_interval: 1h group_by: ['job'] - match_re: owner: 'team-(B|C)' group_by: ['foo', 'bar'] group_wait: 2m receiver: 'notify-BC' - match: group_by: 'role' group_by: ['role'] routes: - match: env: 'testing' receiver: 'notify-testing' routes: - match: wait: 'long' group_wait: 2m ` var ctree config.Route if err := yaml.UnmarshalStrict([]byte(in), &ctree); err != nil { t.Fatal(err) } var ( def = DefaultRouteOpts tree = NewRoute(&ctree, nil) ) lset := func(labels ...string) map[model.LabelName]struct{} { s := map[model.LabelName]struct{}{} for _, ls := range labels { s[model.LabelName(ls)] = struct{}{} } return s } tests := []struct { input model.LabelSet result []*RouteOpts keys []string }{ { input: model.LabelSet{ "owner": "team-A", }, result: []*RouteOpts{ { Receiver: "notify-A", GroupBy: def.GroupBy, GroupWait: def.GroupWait, GroupInterval: def.GroupInterval, RepeatInterval: def.RepeatInterval, }, }, keys: []string{"{}/{owner=\"team-A\"}"}, }, { input: model.LabelSet{ "owner": "team-A", "env": "unset", }, result: []*RouteOpts{ { Receiver: "notify-A", GroupBy: def.GroupBy, GroupWait: def.GroupWait, GroupInterval: def.GroupInterval, RepeatInterval: def.RepeatInterval, }, }, keys: []string{"{}/{owner=\"team-A\"}"}, }, { input: model.LabelSet{ "owner": "team-C", }, result: []*RouteOpts{ { Receiver: "notify-BC", GroupBy: lset("foo", "bar"), GroupWait: 2 * time.Minute, GroupInterval: def.GroupInterval, RepeatInterval: def.RepeatInterval, }, }, keys: []string{"{}/{owner=~\"^(?:team-(B|C))$\"}"}, }, { input: model.LabelSet{ "owner": "team-A", "env": "testing", }, result: []*RouteOpts{ { Receiver: "notify-testing", GroupBy: lset(), GroupWait: def.GroupWait, GroupInterval: def.GroupInterval, RepeatInterval: def.RepeatInterval, }, }, keys: []string{"{}/{owner=\"team-A\"}/{env=\"testing\"}"}, }, { input: model.LabelSet{ "owner": "team-A", "env": "production", }, result: []*RouteOpts{ { Receiver: "notify-productionA", GroupBy: def.GroupBy, GroupWait: 1 * time.Minute, GroupInterval: def.GroupInterval, RepeatInterval: def.RepeatInterval, }, { Receiver: "notify-productionB", GroupBy: lset("job"), GroupWait: 30 * time.Second, GroupInterval: 5 * time.Minute, RepeatInterval: 1 * time.Hour, }, }, keys: []string{ "{}/{owner=\"team-A\"}/{env=\"production\"}", "{}/{owner=\"team-A\"}/{env=~\"^(?:produ.*)$\",job=~\"^(?:.*)$\"}", }, }, { input: model.LabelSet{ "group_by": "role", }, result: []*RouteOpts{ { Receiver: "notify-def", GroupBy: lset("role"), GroupWait: def.GroupWait, GroupInterval: def.GroupInterval, RepeatInterval: def.RepeatInterval, }, }, keys: []string{"{}/{group_by=\"role\"}"}, }, { input: model.LabelSet{ "env": "testing", "group_by": "role", }, result: []*RouteOpts{ { Receiver: "notify-testing", GroupBy: lset("role"), GroupWait: def.GroupWait, GroupInterval: def.GroupInterval, RepeatInterval: def.RepeatInterval, }, }, keys: []string{"{}/{group_by=\"role\"}/{env=\"testing\"}"}, }, { input: model.LabelSet{ "env": "testing", "group_by": "role", "wait": "long", }, result: []*RouteOpts{ { Receiver: "notify-testing", GroupBy: lset("role"), GroupWait: 2 * time.Minute, GroupInterval: def.GroupInterval, RepeatInterval: def.RepeatInterval, }, }, keys: []string{"{}/{group_by=\"role\"}/{env=\"testing\"}/{wait=\"long\"}"}, }, } for _, test := range tests { var matches []*RouteOpts var keys []string for _, r := range tree.Match(test.input) { matches = append(matches, &r.RouteOpts) keys = append(keys, r.Key()) } if !reflect.DeepEqual(matches, test.result) { t.Errorf("\nexpected:\n%v\ngot:\n%v", test.result, matches) } if !reflect.DeepEqual(keys, test.keys) { t.Errorf("\nexpected:\n%v\ngot:\n%v", test.keys, keys) } } } prometheus-alertmanager-0.15.3+ds/doc/000077500000000000000000000000001341674552200176135ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/doc/arch.svg000066400000000000000000001235551341674552200212640ustar00rootroot00000000000000
Notification Pipeline
Notification Pipeline
Receiver
E-Mail
[Not supported by viewer]
Set Notifies
Set Notifies
Receiver
Webhook
[Not supported by viewer]
Dispatcher
Dispatcher<br>
Gossip
Settle
[Not supported by viewer]
API
API
Alert Generators
(Prometheus)
[Not supported by viewer]
Silence Provider
Silence Provider
Silencer
Silencer
Router
Router
Wait
Wait
Wait
Wait
Already
sent?
Already<br>sent?
Retry
Retry
Dedup
Dedup
Retry
Retry
Set Notifies
Set Notifies
Notify Provider
Notify Provider
Alert Provider
Alert Provider
Subscribe
Subscribe
Inhibitor
Inhibitor
Cluster
Cluster<br>
Peers
Peers
Store
Store
Store
Store
Silences
Silences
Notification
Logs
Notification<br>Logs
Dedup
Dedup
High Availability mode
High Availability mode<br>
Aggregate
Aggregate
Group
Group
Group
Group
...
...
. . .
. . .
prometheus-alertmanager-0.15.3+ds/doc/arch.xml000066400000000000000000000105071341674552200212550ustar00rootroot000000000000007V1bk9o6Ev41VO0+QPlu8ziXkHOqkq2pzFZlz6OwBWjjQaxtMpPz61eyLWOrZTAgG5hM5iEgy7Ksr7vVVzGyH17ePidos/pKIxyPLCN6G9mPI8syHctj//GWX0WLbxlFwzIhUdlp1/BM/sZlo+i2JRFOGx0zSuOMbJqNIV2vcZg12lCS0NdmtwWNm0/doCUGDc8himHrdxJlq6I1cI1d+x+YLFfiyaZRXnlBonPZkK5QRF9rTfankf2QUJoVn17eHnDMF0+sS3HfrOVqNbEEr7MuNzg4jEIrChfRFOEpDsemNS3G+Inibfm6/6IZWZAQZYSu2ZUnssExWePyDbJfYlleVyTDzxsU8u+vDPqRfb/KXmL2zWQfaYLWfF3vFySOH2hMk/w2ezb79PDAJni/TFBE2MTFtTVd8+7VGhm8T4zStPycZgn9gWsjBQb/Y1cilK5wVD73J04yNvv4LibLNWvLKJ/Ygq6z53LuvNeKJuRv1obEdPMOBemZdvl9hl5IzIn2LiG84325VuwJ+K0VArMClnEEpi84S36xLuUNjiCbkhksv/j6uqMsyy67rGpU5UztkqJLal5WQ+8AZx9KzFvw9xbIwf4cG35gRNF8bHsA/pHlxeyZ93P2Yck/fMMhJuydxQX2kOpa1TmRWyLyUzR9Gn9FJK7dXrt2LFUVRCDY0DyKXu7zv1YSSeh2HeVkxO9G5bWQ4crevT+CcCWCcCwHUISvIghTAz0AeeC5gCCeccYaCqnAxPAHYDJg/rQTYIKr9TIwxEvBgSfxtMyoYNTveL6i9Ee7DLgRVu9N1AtmOpazdRDKIkBTy4yiaYiCIPDHJtzoASBwxetwrNCG93t5W3I1b7KI6Wu4Qkk22SQ0xByLexWifa2uZR/aR01TxYX++YtrgaV8JOkGZeFqx1F1btClOM3sT94exUlmAiP/dxwnddOeesN0elA5UoJquhpYxjQBrJ9pmpINhJRtiVl8tEZ8jkCz7el0NlMA9EKiiD9/MJnmNRAKzG4izdWAD9zq7p7+PB+ENjYbiuidpiATLFBf0UBB8pYOkg/gksbsXVjTZ7zGCcpoktbJv7bQ3v+23GzNl2Wc5pbTHetgWpu3QgUor8tKAe9/1kD/eEr4m67wNv1nTdMoxm0RucPtbHU70tJDIHZTKNr2cDud0BpqS4mjJRZsQZNsRZd0jeJPu1ZJEtUWGq+jO+6PYV/nMQ1/FE0zEjeZs8aMziP/y/uxmf+HDzlxxde/yifgN5IVlxy//PqXeCL7/IQTwl6c2wSHRCN/t/0gsaWg2yQUvUr+yVCyxNVepAYzwTHKmM7dGEyFTXnrEyU5o5RE4PsNIjBla6KYQnlT3QUkjRM05bcvDVO8HRgmp5PqZU60LS3obHgmMV6z1bQMxtQ/ScT1J5l1S+6MUIZSJo9wCzfW6CxGcxw/0ZTkLqyaSSg2zy9Sh2oTBUakvN3OaZbRlx53A9NvcrvpuxMXMLyn4HcdOhAETdAZBA1CdTmDbih1xxTsM4StD7GAGtA3us1+SyQsu5vm2RMS0Jr+jkh2NA7H6/+dlI6DYqzACpJBc+vWAlvTB2IFAzrLAGwOtNxb9xvykoeF6nCpV7FtU6m2inw7ukfhj2W+vDVUF/k/1iV/2F26KcJXuTtTfFmQNw7IfTmfx1WW8bjXHV8GaxZGa2tCQrpeEAZcMgnZE60Z3yvZf7ydqe+zIgg2RutovF1zt1+K4jEK+TzTMe80TrmPd2a5Hl9TtseyzTqYbNbLo2i08jccpFGzRqMxXmQ55WWoXDgz0ER8nt1UmlS2laWwrUTbOcRnL4w5ngZGEGKEkeOMXftDZJzkX7+syHB9gNA5FlBlqriSpZIyBT4T1lHl0WNtpXlkdLGf/ouz7FeJKtpmNPcmigl+oRzBdleGzKD7TK7RSeZSu3ZZN6Da9B7tJpUj2UKW8M8fsKm0GEMeFAc6Ce36yEkXybgKm1vRa3omyZwuMXwHIHsXJxhFv6AjOc3ffdYb8gLXOvSSJ6Ykjh3yxm0j79t7dxdjYjWYvgqMnSpMRBe6WORalGY54UJL41tFo9dh8gHVIWWzIOvll1yzy4e8TMDVHjIyDyW8/yHhT+JzrxufG2paGEDCezBs8oij7eaKePJKcmNsu1vKQz8c6EPf5Yfs7IrcRWWnDx01N5KGNlgy0WXxgdZLjgyf8EckR8g+iaMU6V/DxXEMiJgI9X8AVl6VQm8i7WIAvIC30BnA7WRcpRa5czs1nE75zHVpmIIZGhqmnDHZRi1nep3kHLdqMge8TgwC9KvWbcM7pHueYzSf400lgiwGPNVShRQLleLn7TwNEzKHmXKMSbMmiSY4JX+jed6BI1++Huvt3o/cxy4CohIlHQIuMtUdJ1PKSqNyuqOqvqdOiC0svc8xYYs1rHwTo7NcE+XI46bHw2ver8VvAaMdBsBcg/wy69Jr55VukRbXJ9t0yS+B4QG3+VAWsh+EbhRMLR8ZwXQauWMRHteVNtaOcVd4DuLcTmIa88XgHmNBIM1+9h3HkPcdKdqhL/UL0IOoVeqXHnxXJ0MPQhCuOzcs17RCFM3nxjwcuxckCNMdjiCc/tXbQzHMfkW/LlEvROmBcFdwKVEvMvs/kGwi2Zr/sR9I73JAekMBeX1g9RA/VstcV5K5Tn8pBxDggTJZbgPgs8NHLQAHLTWfQwAMLfA/1ysyJxmFvr33nyFsCq3zInXZMPj7EG/T7KoKRKsMwZspEDVdeyLF+I2poj5CHJZRx1kETPTiDHnuCeMExqfYOGST4tGu3CyM6ZYn9mpL+NxT73tURm7lYNMBmOfJgNmKchYFXI7XB1x6HROtbvU2bfWUBK3r9Uu1Cb39Km7hvBoicUOen9OLU9IYyQ6CCn2zjr3CKXnT6LcGhRvwq2nkIujrdUIdRv8Q778rl7TI2DgtOXMA9LX6Ka4PRz1h0/3oGeeip7aZTHFLW0S0xREJB5KLpOUzEjQm9AP6UhQ35ykTMtX9BqHPAyqhqMKr/M7GZM/xIC0xzaatN5aITk8yNsA40LqDvNPQZc/aX5sMkYrGDGkIjQcbyGQhYjYfrH8060+tG2F9RfVmeQwCNPVPgpnxZ1oiMwLFsS21mEWq8L9zRh47Z+N7xDldZnPHtkSVyoFUObMPV5sL662aR9jK/rYvdNkraIdKaC8Hm9vML7JFls4lPKTuFQQOb3mbbZxDpNh6W8IDl7C93MFCi+8f6taoz5XkgkHwB3C5/i7gtx8JdAD9waqhZfQ9vS7Xd4pxHb2WxJEeUhEkHc6T4mM9WkyeVlfs7SKvH1TPOAlUMJDXdgjLEGdjuLdWXztYQfuQ5bSAaaGd9Qd7JGu5+4lIjOYkJhmf/AuNcIcsh90ScmT0H3t0RVFwkQQkkoICGAI3fa8nHNV1Tc1KwOUywUuUQZdVk8478VsNi2n+T1pk5WlTvRVhGs3TlV14onjgw4WXs7b0LDz0E35m6wvFWg+LXj8FoHQqNSvNrT5BkCphFcewqbxFOo5hgyCofvFGWv+PIwAvfQTgeGpoIj3Hl/KPTFchfFUHrGugPlDeYWtVfAfIwQUIPzzsQ/hyiq/ZjCyAvNuuii8YyOhN8YXUAUXR7dZSHCkbulLOoRS0Tv6QIlllAH8IhNj+EAD9CIBpM/fjdAEgD9RRABxbn9/2HF0F+pD03lPN3YWkS6e6a+/c40pPli4uNO1Uyu1Omat+ORO41OrLW/rUpHID1+B/oxPOfKnUYrU2TbcZ/8HRh+rnXI0R/JEWuliQEDNldx3iTZZOwpKcoGNGZXeWGifQsHuzgLxm5HXqKYoWTIUHwNSQBd9aZPzbG6KmBX9Lpy9LFKLQ4QTgD2a9AmY1LVWJ0XDcqvqJT3kj3+2rIbfPSXiANxvbZX3Rm5Dsy1E8c6Pdf/ZSi4JeW39Xwaei7Uxd0jabYiKwTlMlbUcSNyZId9OkTMozFod79KZMuh3caDdOlC3b5rUQZfUr8+dTZbcS5bNpsiwP648muxy4fGs0eTXkJutAnclN1rm8gURg+Rxd5GZ6vmNbC8+NHMPDEVL9ovJkMgEUdzix9Ng00irjG2ak1rU+ZdV32dghfK0q6Rb07za99fqc9SZw1kMF3VYp6McHStnXhPIfFt3RA9dVv9II8x7/Bw==prometheus-alertmanager-0.15.3+ds/doc/examples/000077500000000000000000000000001341674552200214315ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/doc/examples/simple.yml000066400000000000000000000067221341674552200234540ustar00rootroot00000000000000global: # The smarthost and SMTP sender used for mail notifications. smtp_smarthost: 'localhost:25' smtp_from: 'alertmanager@example.org' smtp_auth_username: 'alertmanager' smtp_auth_password: 'password' # The auth token for Hipchat. hipchat_auth_token: '1234556789' # Alternative host for Hipchat. hipchat_api_url: 'https://hipchat.foobar.org/' # The directory from which notification templates are read. templates: - '/etc/alertmanager/template/*.tmpl' # The root route on which each incoming alert enters. route: # The labels by which incoming alerts are grouped together. For example, # multiple alerts coming in for cluster=A and alertname=LatencyHigh would # be batched into a single group. group_by: ['alertname', 'cluster', 'service'] # When a new group of alerts is created by an incoming alert, wait at # least 'group_wait' to send the initial notification. # This way ensures that you get multiple alerts for the same group that start # firing shortly after another are batched together on the first # notification. group_wait: 30s # When the first notification was sent, wait 'group_interval' to send a batch # of new alerts that started firing for that group. group_interval: 5m # If an alert has successfully been sent, wait 'repeat_interval' to # resend them. repeat_interval: 3h # A default receiver receiver: team-X-mails # All the above attributes are inherited by all child routes and can # overwritten on each. # The child route trees. routes: # This routes performs a regular expression match on alert labels to # catch alerts that are related to a list of services. - match_re: service: ^(foo1|foo2|baz)$ receiver: team-X-mails # The service has a sub-route for critical alerts, any alerts # that do not match, i.e. severity != critical, fall-back to the # parent node and are sent to 'team-X-mails' routes: - match: severity: critical receiver: team-X-pager - match: service: files receiver: team-Y-mails routes: - match: severity: critical receiver: team-Y-pager # This route handles all alerts coming from a database service. If there's # no team to handle it, it defaults to the DB team. - match: service: database receiver: team-DB-pager # Also group alerts by affected database. group_by: [alertname, cluster, database] routes: - match: owner: team-X receiver: team-X-pager - match: owner: team-Y receiver: team-Y-pager # Inhibition rules allow to mute a set of alerts given that another alert is # firing. # We use this to mute any warning-level notifications if the same alert is # already critical. inhibit_rules: - source_match: severity: 'critical' target_match: severity: 'warning' # Apply inhibition if the alertname is the same. equal: ['alertname', 'cluster', 'service'] receivers: - name: 'team-X-mails' email_configs: - to: 'team-X+alerts@example.org' - name: 'team-X-pager' email_configs: - to: 'team-X+alerts-critical@example.org' pagerduty_configs: - service_key: - name: 'team-Y-mails' email_configs: - to: 'team-Y+alerts@example.org' - name: 'team-Y-pager' pagerduty_configs: - service_key: - name: 'team-DB-pager' pagerduty_configs: - service_key: - name: 'team-X-hipchat' hipchat_configs: - auth_token: room_id: 85 message_format: html notify: true prometheus-alertmanager-0.15.3+ds/examples/000077500000000000000000000000001341674552200206645ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/examples/ha/000077500000000000000000000000001341674552200212545ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/examples/ha/alertmanager.yml000066400000000000000000000005741341674552200244470ustar00rootroot00000000000000global: resolve_timeout: 5m route: group_by: ['alertname'] group_wait: 10s group_interval: 10s repeat_interval: 1h receiver: 'web.hook' receivers: - name: 'web.hook' webhook_configs: - url: 'http://127.0.0.1:5001/' inhibit_rules: - source_match: severity: 'critical' target_match: severity: 'warning' equal: ['alertname', 'dev', 'instance'] prometheus-alertmanager-0.15.3+ds/examples/ha/send_alerts.sh000077500000000000000000000031201341674552200241120ustar00rootroot00000000000000alerts1='[ { "labels": { "alertname": "DiskRunningFull", "dev": "sda1", "instance": "example1" }, "annotations": { "info": "The disk sda1 is running full", "summary": "please check the instance example1" } }, { "labels": { "alertname": "DiskRunningFull", "dev": "sda2", "instance": "example1" }, "annotations": { "info": "The disk sda2 is running full", "summary": "please check the instance example1", "runbook": "the following link http://test-url should be clickable" } }, { "labels": { "alertname": "DiskRunningFull", "dev": "sda1", "instance": "example2" }, "annotations": { "info": "The disk sda1 is running full", "summary": "please check the instance example2" } }, { "labels": { "alertname": "DiskRunningFull", "dev": "sdb2", "instance": "example2" }, "annotations": { "info": "The disk sdb2 is running full", "summary": "please check the instance example2" } }, { "labels": { "alertname": "DiskRunningFull", "dev": "sda1", "instance": "example3", "severity": "critical" } }, { "labels": { "alertname": "DiskRunningFull", "dev": "sda1", "instance": "example3", "severity": "warning" } } ]' curl -XPOST -d"$alerts1" http://localhost:9093/api/v1/alerts curl -XPOST -d"$alerts1" http://localhost:9094/api/v1/alerts curl -XPOST -d"$alerts1" http://localhost:9095/api/v1/alerts prometheus-alertmanager-0.15.3+ds/examples/webhook/000077500000000000000000000000001341674552200223225ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/examples/webhook/echo.go000066400000000000000000000017721341674552200235760ustar00rootroot00000000000000// Copyright 2018 Prometheus Team // 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 ( "bytes" "encoding/json" "io/ioutil" "log" "net/http" ) func main() { log.Fatal(http.ListenAndServe(":5001", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { b, err := ioutil.ReadAll(r.Body) if err != nil { panic(err) } defer r.Body.Close() var buf bytes.Buffer if err := json.Indent(&buf, b, " >", " "); err != nil { panic(err) } log.Println(buf.String()) }))) } prometheus-alertmanager-0.15.3+ds/inhibit/000077500000000000000000000000001341674552200204745ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/inhibit/inhibit.go000066400000000000000000000140361341674552200224550ustar00rootroot00000000000000// Copyright 2015 Prometheus Team // 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 inhibit import ( "context" "sync" "time" "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" "github.com/oklog/oklog/pkg/group" "github.com/prometheus/common/model" "github.com/prometheus/alertmanager/config" "github.com/prometheus/alertmanager/provider" "github.com/prometheus/alertmanager/types" ) // An Inhibitor determines whether a given label set is muted // based on the currently active alerts and a set of inhibition rules. type Inhibitor struct { alerts provider.Alerts rules []*InhibitRule marker types.Marker logger log.Logger mtx sync.RWMutex cancel func() } // NewInhibitor returns a new Inhibitor. func NewInhibitor(ap provider.Alerts, rs []*config.InhibitRule, mk types.Marker, logger log.Logger) *Inhibitor { ih := &Inhibitor{ alerts: ap, marker: mk, logger: logger, } for _, cr := range rs { r := NewInhibitRule(cr) ih.rules = append(ih.rules, r) } return ih } func (ih *Inhibitor) runGC(ctx context.Context) { for { select { case <-time.After(15 * time.Minute): for _, r := range ih.rules { r.gc() } case <-ctx.Done(): return } } } func (ih *Inhibitor) run(ctx context.Context) { it := ih.alerts.Subscribe() defer it.Close() for { select { case <-ctx.Done(): return case a := <-it.Next(): if err := it.Err(); err != nil { level.Error(ih.logger).Log("msg", "Error iterating alerts", "err", err) continue } // Update the inhibition rules' cache. for _, r := range ih.rules { if r.SourceMatchers.Match(a.Labels) { r.set(a) } } } } } // Run the Inihibitor's background processing. func (ih *Inhibitor) Run() { var ( g group.Group ctx context.Context ) ih.mtx.Lock() ctx, ih.cancel = context.WithCancel(context.Background()) ih.mtx.Unlock() gcCtx, gcCancel := context.WithCancel(ctx) runCtx, runCancel := context.WithCancel(ctx) g.Add(func() error { ih.runGC(gcCtx) return nil }, func(err error) { gcCancel() }) g.Add(func() error { ih.run(runCtx) return nil }, func(err error) { runCancel() }) if err := g.Run(); err != nil { level.Warn(ih.logger).Log("msg", "error running inhibitor", "err", err) } } // Stop the Inhibitor's background processing. func (ih *Inhibitor) Stop() { if ih == nil { return } ih.mtx.RLock() defer ih.mtx.RUnlock() if ih.cancel != nil { ih.cancel() } } // Mutes returns true iff the given label set is muted. func (ih *Inhibitor) Mutes(lset model.LabelSet) bool { fp := lset.Fingerprint() for _, r := range ih.rules { // Only inhibit if target matchers match but source matchers don't. if inhibitedByFP, eq := r.hasEqual(lset); !r.SourceMatchers.Match(lset) && r.TargetMatchers.Match(lset) && eq { ih.marker.SetInhibited(fp, inhibitedByFP.String()) return true } } ih.marker.SetInhibited(fp) return false } // An InhibitRule specifies that a class of (source) alerts should inhibit // notifications for another class of (target) alerts if all specified matching // labels are equal between the two alerts. This may be used to inhibit alerts // from sending notifications if their meaning is logically a subset of a // higher-level alert. type InhibitRule struct { // The set of Filters which define the group of source alerts (which inhibit // the target alerts). SourceMatchers types.Matchers // The set of Filters which define the group of target alerts (which are // inhibited by the source alerts). TargetMatchers types.Matchers // A set of label names whose label values need to be identical in source and // target alerts in order for the inhibition to take effect. Equal map[model.LabelName]struct{} mtx sync.RWMutex // Cache of alerts matching source labels. scache map[model.Fingerprint]*types.Alert } // NewInhibitRule returns a new InihibtRule based on a configuration definition. func NewInhibitRule(cr *config.InhibitRule) *InhibitRule { var ( sourcem types.Matchers targetm types.Matchers ) for ln, lv := range cr.SourceMatch { sourcem = append(sourcem, types.NewMatcher(model.LabelName(ln), lv)) } for ln, lv := range cr.SourceMatchRE { sourcem = append(sourcem, types.NewRegexMatcher(model.LabelName(ln), lv.Regexp)) } for ln, lv := range cr.TargetMatch { targetm = append(targetm, types.NewMatcher(model.LabelName(ln), lv)) } for ln, lv := range cr.TargetMatchRE { targetm = append(targetm, types.NewRegexMatcher(model.LabelName(ln), lv.Regexp)) } equal := map[model.LabelName]struct{}{} for _, ln := range cr.Equal { equal[ln] = struct{}{} } return &InhibitRule{ SourceMatchers: sourcem, TargetMatchers: targetm, Equal: equal, scache: map[model.Fingerprint]*types.Alert{}, } } // set the alert in the source cache. func (r *InhibitRule) set(a *types.Alert) { r.mtx.Lock() defer r.mtx.Unlock() r.scache[a.Fingerprint()] = a } // hasEqual checks whether the source cache contains alerts matching // the equal labels for the given label set. func (r *InhibitRule) hasEqual(lset model.LabelSet) (model.Fingerprint, bool) { r.mtx.RLock() defer r.mtx.RUnlock() Outer: for fp, a := range r.scache { // The cache might be stale and contain resolved alerts. if a.Resolved() { continue } for n := range r.Equal { if a.Labels[n] != lset[n] { continue Outer } } return fp, true } return model.Fingerprint(0), false } // gc clears out resolved alerts from the source cache. func (r *InhibitRule) gc() { r.mtx.Lock() defer r.mtx.Unlock() for fp, a := range r.scache { if a.Resolved() { delete(r.scache, fp) } } } prometheus-alertmanager-0.15.3+ds/inhibit/inhibit_test.go000066400000000000000000000225651341674552200235220ustar00rootroot00000000000000// Copyright 2016 Prometheus Team // 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 inhibit import ( "reflect" "testing" "time" "github.com/go-kit/kit/log" "github.com/kylelemons/godebug/pretty" "github.com/prometheus/common/model" "github.com/prometheus/alertmanager/config" "github.com/prometheus/alertmanager/provider" "github.com/prometheus/alertmanager/types" ) var nopLogger = log.NewNopLogger() func TestInhibitRuleHasEqual(t *testing.T) { t.Parallel() now := time.Now() cases := []struct { initial map[model.Fingerprint]*types.Alert equal model.LabelNames input model.LabelSet result bool }{ { // No source alerts at all. initial: map[model.Fingerprint]*types.Alert{}, input: model.LabelSet{"a": "b"}, result: false, }, { // No equal labels, any source alerts satisfies the requirement. initial: map[model.Fingerprint]*types.Alert{1: &types.Alert{}}, input: model.LabelSet{"a": "b"}, result: true, }, { // Matching but already resolved. initial: map[model.Fingerprint]*types.Alert{ 1: &types.Alert{ Alert: model.Alert{ Labels: model.LabelSet{"a": "b", "b": "f"}, StartsAt: now.Add(-time.Minute), EndsAt: now.Add(-time.Second), }, }, 2: &types.Alert{ Alert: model.Alert{ Labels: model.LabelSet{"a": "b", "b": "c"}, StartsAt: now.Add(-time.Minute), EndsAt: now.Add(-time.Second), }, }, }, equal: model.LabelNames{"a", "b"}, input: model.LabelSet{"a": "b", "b": "c"}, result: false, }, { // Matching and unresolved. initial: map[model.Fingerprint]*types.Alert{ 1: &types.Alert{ Alert: model.Alert{ Labels: model.LabelSet{"a": "b", "c": "d"}, StartsAt: now.Add(-time.Minute), EndsAt: now.Add(-time.Second), }, }, 2: &types.Alert{ Alert: model.Alert{ Labels: model.LabelSet{"a": "b", "c": "f"}, StartsAt: now.Add(-time.Minute), EndsAt: now.Add(time.Hour), }, }, }, equal: model.LabelNames{"a"}, input: model.LabelSet{"a": "b"}, result: true, }, { // Equal label does not match. initial: map[model.Fingerprint]*types.Alert{ 1: &types.Alert{ Alert: model.Alert{ Labels: model.LabelSet{"a": "c", "c": "d"}, StartsAt: now.Add(-time.Minute), EndsAt: now.Add(-time.Second), }, }, 2: &types.Alert{ Alert: model.Alert{ Labels: model.LabelSet{"a": "c", "c": "f"}, StartsAt: now.Add(-time.Minute), EndsAt: now.Add(-time.Second), }, }, }, equal: model.LabelNames{"a"}, input: model.LabelSet{"a": "b"}, result: false, }, } for _, c := range cases { r := &InhibitRule{ Equal: map[model.LabelName]struct{}{}, scache: map[model.Fingerprint]*types.Alert{}, } for _, ln := range c.equal { r.Equal[ln] = struct{}{} } for k, v := range c.initial { r.scache[k] = v } if _, have := r.hasEqual(c.input); have != c.result { t.Errorf("Unexpected result %t, expected %t", have, c.result) } if !reflect.DeepEqual(r.scache, c.initial) { t.Errorf("Cache state unexpectedly changed") t.Errorf(pretty.Compare(r.scache, c.initial)) } } } func TestInhibitRuleMatches(t *testing.T) { t.Parallel() // Simple inhibut rule cr := config.InhibitRule{ SourceMatch: map[string]string{"s": "1"}, TargetMatch: map[string]string{"t": "1"}, Equal: model.LabelNames{"e"}, } m := types.NewMarker() ih := NewInhibitor(nil, []*config.InhibitRule{&cr}, m, nopLogger) ir := ih.rules[0] now := time.Now() // Active alert that matches the source filter sourceAlert := types.Alert{ Alert: model.Alert{ Labels: model.LabelSet{"s": "1", "e": "1"}, StartsAt: now.Add(-time.Minute), EndsAt: now.Add(time.Hour), }, } ir.scache = map[model.Fingerprint]*types.Alert{1: &sourceAlert} cases := []struct { target model.LabelSet expected bool }{ { // Matches target filter, inhibited target: model.LabelSet{"t": "1", "e": "1"}, expected: true, }, { // Matches target filter (plus noise), inhibited target: model.LabelSet{"t": "1", "t2": "1", "e": "1"}, expected: true, }, { // Doesn't match target filter, not inhibited target: model.LabelSet{"t": "0", "e": "1"}, expected: false, }, { // Matches both source and target filters, not inhibited target: model.LabelSet{"s": "1", "t": "1", "e": "1"}, expected: false, }, { // Matches target filter, equal label doesn't match, not inhibited target: model.LabelSet{"t": "1", "e": "0"}, expected: false, }, } for _, c := range cases { if actual := ih.Mutes(c.target); actual != c.expected { t.Errorf("Expected (*Inhibitor).Mutes(%v) to return %t but got %t", c.target, c.expected, actual) } } } func TestInhibitRuleGC(t *testing.T) { // TODO(fabxc): add now() injection function to Resolved() to remove // dependency on machine time in this test. now := time.Now() newAlert := func(start, end time.Duration) *types.Alert { return &types.Alert{ Alert: model.Alert{ Labels: model.LabelSet{"a": "b"}, StartsAt: now.Add(start * time.Minute), EndsAt: now.Add(end * time.Minute), }, } } before := map[model.Fingerprint]*types.Alert{ 0: newAlert(-10, -5), 1: newAlert(10, 20), 2: newAlert(-10, 10), 3: newAlert(-10, -1), } after := map[model.Fingerprint]*types.Alert{ 1: newAlert(10, 20), 2: newAlert(-10, 10), } r := &InhibitRule{scache: before} r.gc() if !reflect.DeepEqual(r.scache, after) { t.Errorf("Unexpected cache state after GC") t.Errorf(pretty.Compare(r.scache, after)) } } type fakeAlerts struct { alerts []*types.Alert finished chan struct{} } func newFakeAlerts(alerts []*types.Alert) *fakeAlerts { return &fakeAlerts{ alerts: alerts, finished: make(chan struct{}), } } func (f *fakeAlerts) GetPending() provider.AlertIterator { return nil } func (f *fakeAlerts) Get(model.Fingerprint) (*types.Alert, error) { return nil, nil } func (f *fakeAlerts) Put(...*types.Alert) error { return nil } func (f *fakeAlerts) Subscribe() provider.AlertIterator { ch := make(chan *types.Alert) done := make(chan struct{}) go func() { for _, a := range f.alerts { ch <- a } // Send another (meaningless) alert to make sure that the inhibitor has // processed everything. ch <- &types.Alert{ Alert: model.Alert{ Labels: model.LabelSet{}, StartsAt: time.Now(), }, } close(f.finished) <-done }() return provider.NewAlertIterator(ch, done, nil) } func TestInhibit(t *testing.T) { t.Parallel() now := time.Now() inhibitRule := func() *config.InhibitRule { return &config.InhibitRule{ SourceMatch: map[string]string{"s": "1"}, TargetMatch: map[string]string{"t": "1"}, Equal: model.LabelNames{"e"}, } } // alertOne is muted by alertTwo when it is active. alertOne := func() *types.Alert { return &types.Alert{ Alert: model.Alert{ Labels: model.LabelSet{"t": "1", "e": "f"}, StartsAt: now.Add(-time.Minute), EndsAt: now.Add(time.Hour), }, } } alertTwo := func(resolved bool) *types.Alert { var end time.Time if resolved { end = now.Add(-time.Second) } else { end = now.Add(time.Hour) } return &types.Alert{ Alert: model.Alert{ Labels: model.LabelSet{"s": "1", "e": "f"}, StartsAt: now.Add(-time.Minute), EndsAt: end, }, } } type exp struct { lbls model.LabelSet muted bool } for i, tc := range []struct { alerts []*types.Alert expected []exp }{ { // alertOne shouldn't be muted since alertTwo hasn't fired. alerts: []*types.Alert{alertOne()}, expected: []exp{ { lbls: model.LabelSet{"t": "1", "e": "f"}, muted: false, }, }, }, { // alertOne should be muted by alertTwo which is active. alerts: []*types.Alert{alertOne(), alertTwo(false)}, expected: []exp{ { lbls: model.LabelSet{"t": "1", "e": "f"}, muted: true, }, { lbls: model.LabelSet{"s": "1", "e": "f"}, muted: false, }, }, }, { // alertOne shouldn't be muted since alertTwo is resolved. alerts: []*types.Alert{alertOne(), alertTwo(false), alertTwo(true)}, expected: []exp{ { lbls: model.LabelSet{"t": "1", "e": "f"}, muted: false, }, { lbls: model.LabelSet{"s": "1", "e": "f"}, muted: false, }, }, }, } { ap := newFakeAlerts(tc.alerts) mk := types.NewMarker() inhibitor := NewInhibitor(ap, []*config.InhibitRule{inhibitRule()}, mk, nopLogger) go func() { for ap.finished != nil { select { case <-ap.finished: ap.finished = nil default: } } inhibitor.Stop() }() inhibitor.Run() for _, expected := range tc.expected { if inhibitor.Mutes(expected.lbls) != expected.muted { mute := "unmuted" if expected.muted { mute = "muted" } t.Errorf("tc: %d, expected alert with labels %q to be %s", i, expected.lbls, mute) } } } } prometheus-alertmanager-0.15.3+ds/nflog/000077500000000000000000000000001341674552200201535ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/nflog/nflog.go000066400000000000000000000336431341674552200216200ustar00rootroot00000000000000// Copyright 2016 Prometheus Team // 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 nflog implements a garbage-collected and snapshottable append-only log of // active/resolved notifications. Each log entry stores the active/resolved state, // the notified receiver, and a hash digest of the notification's identifying contents. // The log can be queried along different parameters. package nflog import ( "bytes" "errors" "fmt" "io" "math/rand" "os" "sync" "time" "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" "github.com/matttproud/golang_protobuf_extensions/pbutil" "github.com/prometheus/alertmanager/cluster" pb "github.com/prometheus/alertmanager/nflog/nflogpb" "github.com/prometheus/client_golang/prometheus" ) // ErrNotFound is returned for empty query results. var ErrNotFound = errors.New("not found") // ErrInvalidState is returned if the state isn't valid. var ErrInvalidState = fmt.Errorf("invalid state") // query currently allows filtering by and/or receiver group key. // It is configured via QueryParameter functions. // // TODO(fabxc): Future versions could allow querying a certain receiver // group or a given time interval. type query struct { recv *pb.Receiver groupKey string } // QueryParam is a function that modifies a query to incorporate // a set of parameters. Returns an error for invalid or conflicting // parameters. type QueryParam func(*query) error // QReceiver adds a receiver parameter to a query. func QReceiver(r *pb.Receiver) QueryParam { return func(q *query) error { q.recv = r return nil } } // QGroupKey adds a group key as querying argument. func QGroupKey(gk string) QueryParam { return func(q *query) error { q.groupKey = gk return nil } } type Log struct { logger log.Logger metrics *metrics now func() time.Time retention time.Duration runInterval time.Duration snapf string stopc chan struct{} done func() // For now we only store the most recently added log entry. // The key is a serialized concatenation of group key and receiver. mtx sync.RWMutex st state broadcast func([]byte) } type metrics struct { gcDuration prometheus.Summary snapshotDuration prometheus.Summary snapshotSize prometheus.Gauge queriesTotal prometheus.Counter queryErrorsTotal prometheus.Counter queryDuration prometheus.Histogram propagatedMessagesTotal prometheus.Counter } func newMetrics(r prometheus.Registerer) *metrics { m := &metrics{} m.gcDuration = prometheus.NewSummary(prometheus.SummaryOpts{ Name: "alertmanager_nflog_gc_duration_seconds", Help: "Duration of the last notification log garbage collection cycle.", }) m.snapshotDuration = prometheus.NewSummary(prometheus.SummaryOpts{ Name: "alertmanager_nflog_snapshot_duration_seconds", Help: "Duration of the last notification log snapshot.", }) m.snapshotSize = prometheus.NewGauge(prometheus.GaugeOpts{ Name: "alertmanager_nflog_snapshot_size_bytes", Help: "Size of the last notification log snapshot in bytes.", }) m.queriesTotal = prometheus.NewCounter(prometheus.CounterOpts{ Name: "alertmanager_nflog_queries_total", Help: "Number of notification log queries were received.", }) m.queryErrorsTotal = prometheus.NewCounter(prometheus.CounterOpts{ Name: "alertmanager_nflog_query_errors_total", Help: "Number notification log received queries that failed.", }) m.queryDuration = prometheus.NewHistogram(prometheus.HistogramOpts{ Name: "alertmanager_nflog_query_duration_seconds", Help: "Duration of notification log query evaluation.", }) m.propagatedMessagesTotal = prometheus.NewCounter(prometheus.CounterOpts{ Name: "alertmanager_nflog_gossip_messages_propagated_total", Help: "Number of received gossip messages that have been further gossiped.", }) if r != nil { r.MustRegister( m.gcDuration, m.snapshotDuration, m.snapshotSize, m.queriesTotal, m.queryErrorsTotal, m.queryDuration, m.propagatedMessagesTotal, ) } return m } // Option configures a new Log implementation. type Option func(*Log) error // WithRetention sets the retention time for log st. func WithRetention(d time.Duration) Option { return func(l *Log) error { l.retention = d return nil } } // WithNow overwrites the function used to retrieve a timestamp // for the current point in time. // This is generally useful for injection during tests. func WithNow(f func() time.Time) Option { return func(l *Log) error { l.now = f return nil } } // WithLogger configures a logger for the notification log. func WithLogger(logger log.Logger) Option { return func(l *Log) error { l.logger = logger return nil } } // WithMetrics registers metrics for the notification log. func WithMetrics(r prometheus.Registerer) Option { return func(l *Log) error { l.metrics = newMetrics(r) return nil } } // WithMaintenance configures the Log to run garbage collection // and snapshotting, if configured, at the given interval. // // The maintenance terminates on receiving from the provided channel. // The done function is called after the final snapshot was completed. func WithMaintenance(d time.Duration, stopc chan struct{}, done func()) Option { return func(l *Log) error { if d == 0 { return fmt.Errorf("maintenance interval must not be 0") } l.runInterval = d l.stopc = stopc l.done = done return nil } } // WithSnapshot configures the log to be initialized from a given snapshot file. // If maintenance is configured, a snapshot will be saved periodically and on // shutdown as well. func WithSnapshot(sf string) Option { return func(l *Log) error { l.snapf = sf return nil } } func utcNow() time.Time { return time.Now().UTC() } type state map[string]*pb.MeshEntry func (s state) clone() state { c := make(state, len(s)) for k, v := range s { c[k] = v } return c } // merge returns true or false whether the MeshEntry was merged or // not. This information is used to decide to gossip the message further. func (s state) merge(e *pb.MeshEntry) bool { k := stateKey(string(e.Entry.GroupKey), e.Entry.Receiver) prev, ok := s[k] if !ok || prev.Entry.Timestamp.Before(e.Entry.Timestamp) { s[k] = e return true } return false } func (s state) MarshalBinary() ([]byte, error) { var buf bytes.Buffer for _, e := range s { if _, err := pbutil.WriteDelimited(&buf, e); err != nil { return nil, err } } return buf.Bytes(), nil } func decodeState(r io.Reader) (state, error) { st := state{} for { var e pb.MeshEntry _, err := pbutil.ReadDelimited(r, &e) if err == nil { if e.Entry == nil || e.Entry.Receiver == nil { return nil, ErrInvalidState } st[stateKey(string(e.Entry.GroupKey), e.Entry.Receiver)] = &e continue } if err == io.EOF { break } return nil, err } return st, nil } func marshalMeshEntry(e *pb.MeshEntry) ([]byte, error) { var buf bytes.Buffer if _, err := pbutil.WriteDelimited(&buf, e); err != nil { return nil, err } return buf.Bytes(), nil } // New creates a new notification log based on the provided options. // The snapshot is loaded into the Log if it is set. func New(opts ...Option) (*Log, error) { l := &Log{ logger: log.NewNopLogger(), now: utcNow, st: state{}, broadcast: func([]byte) {}, } for _, o := range opts { if err := o(l); err != nil { return nil, err } } if l.metrics == nil { l.metrics = newMetrics(nil) } if l.snapf != "" { if f, err := os.Open(l.snapf); !os.IsNotExist(err) { if err != nil { return l, err } defer f.Close() if err := l.loadSnapshot(f); err != nil { return l, err } } } go l.run() return l, nil } // run periodic background maintenance. func (l *Log) run() { if l.runInterval == 0 || l.stopc == nil { return } t := time.NewTicker(l.runInterval) defer t.Stop() if l.done != nil { defer l.done() } f := func() error { start := l.now() var size int64 level.Debug(l.logger).Log("msg", "Running maintenance") defer func() { level.Debug(l.logger).Log("msg", "Maintenance done", "duration", l.now().Sub(start), "size", size) l.metrics.snapshotSize.Set(float64(size)) }() if _, err := l.GC(); err != nil { return err } if l.snapf == "" { return nil } f, err := openReplace(l.snapf) if err != nil { return err } if size, err = l.Snapshot(f); err != nil { return err } return f.Close() } Loop: for { select { case <-l.stopc: break Loop case <-t.C: if err := f(); err != nil { level.Error(l.logger).Log("msg", "Running maintenance failed", "err", err) } } } // No need to run final maintenance if we don't want to snapshot. if l.snapf == "" { return } if err := f(); err != nil { level.Error(l.logger).Log("msg", "Creating shutdown snapshot failed", "err", err) } } func receiverKey(r *pb.Receiver) string { return fmt.Sprintf("%s/%s/%d", r.GroupName, r.Integration, r.Idx) } // stateKey returns a string key for a log entry consisting of the group key // and receiver. func stateKey(k string, r *pb.Receiver) string { return fmt.Sprintf("%s:%s", k, receiverKey(r)) } func (l *Log) Log(r *pb.Receiver, gkey string, firingAlerts, resolvedAlerts []uint64) error { // Write all st with the same timestamp. now := l.now() key := stateKey(gkey, r) l.mtx.Lock() defer l.mtx.Unlock() if prevle, ok := l.st[key]; ok { // Entry already exists, only overwrite if timestamp is newer. // This may happen with raciness or clock-drift across AM nodes. if prevle.Entry.Timestamp.After(now) { return nil } } e := &pb.MeshEntry{ Entry: &pb.Entry{ Receiver: r, GroupKey: []byte(gkey), Timestamp: now, FiringAlerts: firingAlerts, ResolvedAlerts: resolvedAlerts, }, ExpiresAt: now.Add(l.retention), } b, err := marshalMeshEntry(e) if err != nil { return err } l.st.merge(e) l.broadcast(b) return nil } // GC implements the Log interface. func (l *Log) GC() (int, error) { start := time.Now() defer func() { l.metrics.gcDuration.Observe(time.Since(start).Seconds()) }() now := l.now() var n int l.mtx.Lock() defer l.mtx.Unlock() for k, le := range l.st { if le.ExpiresAt.IsZero() { return n, errors.New("unexpected zero expiration timestamp") } if !le.ExpiresAt.After(now) { delete(l.st, k) n++ } } return n, nil } // Query implements the Log interface. func (l *Log) Query(params ...QueryParam) ([]*pb.Entry, error) { start := time.Now() l.metrics.queriesTotal.Inc() entries, err := func() ([]*pb.Entry, error) { q := &query{} for _, p := range params { if err := p(q); err != nil { return nil, err } } // TODO(fabxc): For now our only query mode is the most recent entry for a // receiver/group_key combination. if q.recv == nil || q.groupKey == "" { // TODO(fabxc): allow more complex queries in the future. // How to enable pagination? return nil, errors.New("no query parameters specified") } l.mtx.RLock() defer l.mtx.RUnlock() if le, ok := l.st[stateKey(q.groupKey, q.recv)]; ok { return []*pb.Entry{le.Entry}, nil } return nil, ErrNotFound }() if err != nil { l.metrics.queryErrorsTotal.Inc() } l.metrics.queryDuration.Observe(time.Since(start).Seconds()) return entries, err } // loadSnapshot loads a snapshot generated by Snapshot() into the state. func (l *Log) loadSnapshot(r io.Reader) error { st, err := decodeState(r) if err != nil { return err } l.mtx.Lock() l.st = st l.mtx.Unlock() return nil } // Snapshot implements the Log interface. func (l *Log) Snapshot(w io.Writer) (int64, error) { start := time.Now() defer func() { l.metrics.snapshotDuration.Observe(time.Since(start).Seconds()) }() l.mtx.RLock() defer l.mtx.RUnlock() b, err := l.st.MarshalBinary() if err != nil { return 0, err } return io.Copy(w, bytes.NewReader(b)) } // MarshalBinary serializes all contents of the notification log. func (l *Log) MarshalBinary() ([]byte, error) { l.mtx.Lock() defer l.mtx.Unlock() return l.st.MarshalBinary() } // Merge merges notification log state received from the cluster with the local state. func (l *Log) Merge(b []byte) error { st, err := decodeState(bytes.NewReader(b)) if err != nil { return err } l.mtx.Lock() defer l.mtx.Unlock() for _, e := range st { if merged := l.st.merge(e); merged && !cluster.OversizedMessage(b) { // If this is the first we've seen the message and it's // not oversized, gossip it to other nodes. We don't // propagate oversized messages because they're sent to // all nodes already. l.broadcast(b) l.metrics.propagatedMessagesTotal.Inc() level.Debug(l.logger).Log("msg", "gossiping new entry", "entry", e) } } return nil } // SetBroadcast sets a broadcast callback that will be invoked with serialized state // on updates. func (l *Log) SetBroadcast(f func([]byte)) { l.mtx.Lock() l.broadcast = f l.mtx.Unlock() } // replaceFile wraps a file that is moved to another filename on closing. type replaceFile struct { *os.File filename string } func (f *replaceFile) Close() error { if err := f.File.Sync(); err != nil { return err } if err := f.File.Close(); err != nil { return err } return os.Rename(f.File.Name(), f.filename) } // openReplace opens a new temporary file that is moved to filename on closing. func openReplace(filename string) (*replaceFile, error) { tmpFilename := fmt.Sprintf("%s.%x", filename, uint64(rand.Int63())) f, err := os.Create(tmpFilename) if err != nil { return nil, err } rf := &replaceFile{ File: f, filename: filename, } return rf, nil } prometheus-alertmanager-0.15.3+ds/nflog/nflog_test.go000066400000000000000000000211171341674552200226500ustar00rootroot00000000000000// Copyright 2016 Prometheus Team // 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 nflog import ( "bytes" "io/ioutil" "os" "path/filepath" "testing" "time" pb "github.com/prometheus/alertmanager/nflog/nflogpb" "github.com/stretchr/testify/require" ) func TestLogGC(t *testing.T) { now := utcNow() // We only care about key names and expiration timestamps. newEntry := func(ts time.Time) *pb.MeshEntry { return &pb.MeshEntry{ ExpiresAt: ts, } } l := &Log{ st: state{ "a1": newEntry(now), "a2": newEntry(now.Add(time.Second)), "a3": newEntry(now.Add(-time.Second)), }, now: func() time.Time { return now }, metrics: newMetrics(nil), } n, err := l.GC() require.NoError(t, err, "unexpected error in garbage collection") require.Equal(t, 2, n, "unexpected number of removed entries") expected := state{ "a2": newEntry(now.Add(time.Second)), } require.Equal(t, l.st, expected, "unepexcted state after garbage collection") } func TestLogSnapshot(t *testing.T) { // Check whether storing and loading the snapshot is symmetric. now := utcNow() cases := []struct { entries []*pb.MeshEntry }{ { entries: []*pb.MeshEntry{ { Entry: &pb.Entry{ GroupKey: []byte("d8e8fca2dc0f896fd7cb4cb0031ba249"), Receiver: &pb.Receiver{GroupName: "abc", Integration: "test1", Idx: 1}, GroupHash: []byte("126a8a51b9d1bbd07fddc65819a542c3"), Resolved: false, Timestamp: now, }, ExpiresAt: now, }, { Entry: &pb.Entry{ GroupKey: []byte("d8e8fca2dc0f8abce7cb4cb0031ba249"), Receiver: &pb.Receiver{GroupName: "def", Integration: "test2", Idx: 29}, GroupHash: []byte("122c2331b9d1bbd07fddc65819a542c3"), Resolved: true, Timestamp: now, }, ExpiresAt: now, }, { Entry: &pb.Entry{ GroupKey: []byte("aaaaaca2dc0f896fd7cb4cb0031ba249"), Receiver: &pb.Receiver{GroupName: "ghi", Integration: "test3", Idx: 0}, GroupHash: []byte("126a8a51b9d1bbd07fddc6e3e3e542c3"), Resolved: false, Timestamp: now, }, ExpiresAt: now, }, }, }, } for _, c := range cases { f, err := ioutil.TempFile("", "snapshot") require.NoError(t, err, "creating temp file failed") l1 := &Log{ st: state{}, metrics: newMetrics(nil), } // Setup internal state manually. for _, e := range c.entries { l1.st[stateKey(string(e.Entry.GroupKey), e.Entry.Receiver)] = e } _, err = l1.Snapshot(f) require.NoError(t, err, "creating snapshot failed") require.NoError(t, f.Close(), "closing snapshot file failed") f, err = os.Open(f.Name()) require.NoError(t, err, "opening snapshot file failed") // Check again against new nlog instance. l2 := &Log{} err = l2.loadSnapshot(f) require.NoError(t, err, "error loading snapshot") require.Equal(t, l1.st, l2.st, "state after loading snapshot did not match snapshotted state") require.NoError(t, f.Close(), "closing snapshot file failed") } } func TestReplaceFile(t *testing.T) { dir, err := ioutil.TempDir("", "replace_file") require.NoError(t, err, "creating temp dir failed") origFilename := filepath.Join(dir, "testfile") of, err := os.Create(origFilename) require.NoError(t, err, "creating file failed") nf, err := openReplace(origFilename) require.NoError(t, err, "opening replacement file failed") _, err = nf.Write([]byte("test")) require.NoError(t, err, "writing replace file failed") require.NotEqual(t, nf.Name(), of.Name(), "replacement file must have different name while editing") require.NoError(t, nf.Close(), "closing replacement file failed") require.NoError(t, of.Close(), "closing original file failed") ofr, err := os.Open(origFilename) require.NoError(t, err, "opening original file failed") defer ofr.Close() res, err := ioutil.ReadAll(ofr) require.NoError(t, err, "reading original file failed") require.Equal(t, "test", string(res), "unexpected file contents") } func TestStateMerge(t *testing.T) { now := utcNow() // We only care about key names and timestamps for the // merging logic. newEntry := func(ts time.Time, name string) *pb.MeshEntry { return &pb.MeshEntry{ Entry: &pb.Entry{ Timestamp: ts, GroupKey: []byte("key"), Receiver: &pb.Receiver{ GroupName: name, Idx: 1, Integration: "integr", }, }, } } cases := []struct { a, b state final state }{ { a: state{ "key:a1/integr/1": newEntry(now, "a1"), "key:a2/integr/1": newEntry(now, "a2"), "key:a3/integr/1": newEntry(now, "a3"), }, b: state{ "key:b1/integr/1": newEntry(now, "b1"), // new key, should be added "key:a2/integr/1": newEntry(now.Add(-time.Minute), "a2"), // older timestamp, should be dropped "key:a3/integr/1": newEntry(now.Add(time.Minute), "a3"), // newer timestamp, should overwrite }, final: state{ "key:a1/integr/1": newEntry(now, "a1"), "key:a2/integr/1": newEntry(now, "a2"), "key:a3/integr/1": newEntry(now.Add(time.Minute), "a3"), "key:b1/integr/1": newEntry(now, "b1"), }, }, } for _, c := range cases { ca, cb := c.a.clone(), c.b.clone() res := c.a.clone() for _, e := range cb { res.merge(e) } require.Equal(t, c.final, res, "Merge result should match expectation") require.Equal(t, c.b, cb, "Merged state should remain unmodified") require.NotEqual(t, c.final, ca, "Merge should not change original state") } } func TestStateDataCoding(t *testing.T) { // Check whether encoding and decoding the data is symmetric. now := utcNow() cases := []struct { entries []*pb.MeshEntry }{ { entries: []*pb.MeshEntry{ { Entry: &pb.Entry{ GroupKey: []byte("d8e8fca2dc0f896fd7cb4cb0031ba249"), Receiver: &pb.Receiver{GroupName: "abc", Integration: "test1", Idx: 1}, GroupHash: []byte("126a8a51b9d1bbd07fddc65819a542c3"), Resolved: false, Timestamp: now, }, ExpiresAt: now, }, { Entry: &pb.Entry{ GroupKey: []byte("d8e8fca2dc0f8abce7cb4cb0031ba249"), Receiver: &pb.Receiver{GroupName: "def", Integration: "test2", Idx: 29}, GroupHash: []byte("122c2331b9d1bbd07fddc65819a542c3"), Resolved: true, Timestamp: now, }, ExpiresAt: now, }, { Entry: &pb.Entry{ GroupKey: []byte("aaaaaca2dc0f896fd7cb4cb0031ba249"), Receiver: &pb.Receiver{GroupName: "ghi", Integration: "test3", Idx: 0}, GroupHash: []byte("126a8a51b9d1bbd07fddc6e3e3e542c3"), Resolved: false, Timestamp: now, }, ExpiresAt: now, }, }, }, } for _, c := range cases { // Create gossip data from input. in := state{} for _, e := range c.entries { in[stateKey(string(e.Entry.GroupKey), e.Entry.Receiver)] = e } msg, err := in.MarshalBinary() require.NoError(t, err) out, err := decodeState(bytes.NewReader(msg)) require.NoError(t, err, "decoding message failed") require.Equal(t, in, out, "decoded data doesn't match encoded data") } } func TestQuery(t *testing.T) { nl, err := New() if err != nil { require.NoError(t, err, "constructing nflog failed") } recv := new(pb.Receiver) // no key param _, err = nl.Query(QGroupKey("key")) require.EqualError(t, err, "no query parameters specified") // no recv param _, err = nl.Query(QReceiver(recv)) require.EqualError(t, err, "no query parameters specified") // no entry _, err = nl.Query(QGroupKey("nonexistingkey"), QReceiver(recv)) require.EqualError(t, err, "not found") // existing entry firingAlerts := []uint64{1, 2, 3} resolvedAlerts := []uint64{4, 5} err = nl.Log(recv, "key", firingAlerts, resolvedAlerts) require.NoError(t, err, "logging notification failed") entries, err := nl.Query(QGroupKey("key"), QReceiver(recv)) require.NoError(t, err, "querying nflog failed") entry := entries[0] require.EqualValues(t, firingAlerts, entry.FiringAlerts) require.EqualValues(t, resolvedAlerts, entry.ResolvedAlerts) } func TestStateDecodingError(t *testing.T) { // Check whether decoding copes with erroneous data. s := state{"": &pb.MeshEntry{}} msg, err := s.MarshalBinary() require.NoError(t, err) _, err = decodeState(bytes.NewReader(msg)) require.Equal(t, ErrInvalidState, err) } prometheus-alertmanager-0.15.3+ds/nflog/nflogpb/000077500000000000000000000000001341674552200216025ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/nflog/nflogpb/nflog.pb.go000066400000000000000000000623341341674552200236460ustar00rootroot00000000000000// Code generated by protoc-gen-gogo. DO NOT EDIT. // source: nflog.proto /* Package nflogpb is a generated protocol buffer package. It is generated from these files: nflog.proto It has these top-level messages: Receiver Entry MeshEntry */ package nflogpb import proto "github.com/gogo/protobuf/proto" import fmt "fmt" import math "math" import _ "github.com/gogo/protobuf/gogoproto" import time "time" import types "github.com/gogo/protobuf/types" import io "io" // Reference imports to suppress errors if they are not otherwise used. var _ = proto.Marshal var _ = fmt.Errorf var _ = math.Inf var _ = time.Kitchen // This is a compile-time assertion to ensure that this generated file // is compatible with the proto package it is being compiled against. // A compilation error at this line likely means your copy of the // proto package needs to be updated. const _ = proto.GoGoProtoPackageIsVersion2 // please upgrade the proto package type Receiver struct { // Configured name of the receiver group. GroupName string `protobuf:"bytes,1,opt,name=group_name,json=groupName,proto3" json:"group_name,omitempty"` // Name of the integration of the receiver. Integration string `protobuf:"bytes,2,opt,name=integration,proto3" json:"integration,omitempty"` // Index of the receiver with respect to the integration. // Every integration in a group may have 0..N configurations. Idx uint32 `protobuf:"varint,3,opt,name=idx,proto3" json:"idx,omitempty"` } func (m *Receiver) Reset() { *m = Receiver{} } func (m *Receiver) String() string { return proto.CompactTextString(m) } func (*Receiver) ProtoMessage() {} func (*Receiver) Descriptor() ([]byte, []int) { return fileDescriptorNflog, []int{0} } // Entry holds information about a successful notification // sent to a receiver. type Entry struct { // The key identifying the dispatching group. GroupKey []byte `protobuf:"bytes,1,opt,name=group_key,json=groupKey,proto3" json:"group_key,omitempty"` // The receiver that was notified. Receiver *Receiver `protobuf:"bytes,2,opt,name=receiver" json:"receiver,omitempty"` // Hash over the state of the group at notification time. // Deprecated in favor of FiringAlerts field, but kept for compatibility. GroupHash []byte `protobuf:"bytes,3,opt,name=group_hash,json=groupHash,proto3" json:"group_hash,omitempty"` // Whether the notification was about a resolved alert. // Deprecated in favor of ResolvedAlerts field, but kept for compatibility. Resolved bool `protobuf:"varint,4,opt,name=resolved,proto3" json:"resolved,omitempty"` // Timestamp of the succeeding notification. Timestamp time.Time `protobuf:"bytes,5,opt,name=timestamp,stdtime" json:"timestamp"` // FiringAlerts list of hashes of firing alerts at the last notification time. FiringAlerts []uint64 `protobuf:"varint,6,rep,packed,name=firing_alerts,json=firingAlerts" json:"firing_alerts,omitempty"` // ResolvedAlerts list of hashes of resolved alerts at the last notification time. ResolvedAlerts []uint64 `protobuf:"varint,7,rep,packed,name=resolved_alerts,json=resolvedAlerts" json:"resolved_alerts,omitempty"` } func (m *Entry) Reset() { *m = Entry{} } func (m *Entry) String() string { return proto.CompactTextString(m) } func (*Entry) ProtoMessage() {} func (*Entry) Descriptor() ([]byte, []int) { return fileDescriptorNflog, []int{1} } // MeshEntry is a wrapper message to communicate a notify log // entry through a mesh network. type MeshEntry struct { // The original raw notify log entry. Entry *Entry `protobuf:"bytes,1,opt,name=entry" json:"entry,omitempty"` // A timestamp indicating when the mesh peer should evict // the log entry from its state. ExpiresAt time.Time `protobuf:"bytes,2,opt,name=expires_at,json=expiresAt,stdtime" json:"expires_at"` } func (m *MeshEntry) Reset() { *m = MeshEntry{} } func (m *MeshEntry) String() string { return proto.CompactTextString(m) } func (*MeshEntry) ProtoMessage() {} func (*MeshEntry) Descriptor() ([]byte, []int) { return fileDescriptorNflog, []int{2} } func init() { proto.RegisterType((*Receiver)(nil), "nflogpb.Receiver") proto.RegisterType((*Entry)(nil), "nflogpb.Entry") proto.RegisterType((*MeshEntry)(nil), "nflogpb.MeshEntry") } func (m *Receiver) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalTo(dAtA) if err != nil { return nil, err } return dAtA[:n], nil } func (m *Receiver) MarshalTo(dAtA []byte) (int, error) { var i int _ = i var l int _ = l if len(m.GroupName) > 0 { dAtA[i] = 0xa i++ i = encodeVarintNflog(dAtA, i, uint64(len(m.GroupName))) i += copy(dAtA[i:], m.GroupName) } if len(m.Integration) > 0 { dAtA[i] = 0x12 i++ i = encodeVarintNflog(dAtA, i, uint64(len(m.Integration))) i += copy(dAtA[i:], m.Integration) } if m.Idx != 0 { dAtA[i] = 0x18 i++ i = encodeVarintNflog(dAtA, i, uint64(m.Idx)) } return i, nil } func (m *Entry) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalTo(dAtA) if err != nil { return nil, err } return dAtA[:n], nil } func (m *Entry) MarshalTo(dAtA []byte) (int, error) { var i int _ = i var l int _ = l if len(m.GroupKey) > 0 { dAtA[i] = 0xa i++ i = encodeVarintNflog(dAtA, i, uint64(len(m.GroupKey))) i += copy(dAtA[i:], m.GroupKey) } if m.Receiver != nil { dAtA[i] = 0x12 i++ i = encodeVarintNflog(dAtA, i, uint64(m.Receiver.Size())) n1, err := m.Receiver.MarshalTo(dAtA[i:]) if err != nil { return 0, err } i += n1 } if len(m.GroupHash) > 0 { dAtA[i] = 0x1a i++ i = encodeVarintNflog(dAtA, i, uint64(len(m.GroupHash))) i += copy(dAtA[i:], m.GroupHash) } if m.Resolved { dAtA[i] = 0x20 i++ if m.Resolved { dAtA[i] = 1 } else { dAtA[i] = 0 } i++ } dAtA[i] = 0x2a i++ i = encodeVarintNflog(dAtA, i, uint64(types.SizeOfStdTime(m.Timestamp))) n2, err := types.StdTimeMarshalTo(m.Timestamp, dAtA[i:]) if err != nil { return 0, err } i += n2 if len(m.FiringAlerts) > 0 { dAtA4 := make([]byte, len(m.FiringAlerts)*10) var j3 int for _, num := range m.FiringAlerts { for num >= 1<<7 { dAtA4[j3] = uint8(uint64(num)&0x7f | 0x80) num >>= 7 j3++ } dAtA4[j3] = uint8(num) j3++ } dAtA[i] = 0x32 i++ i = encodeVarintNflog(dAtA, i, uint64(j3)) i += copy(dAtA[i:], dAtA4[:j3]) } if len(m.ResolvedAlerts) > 0 { dAtA6 := make([]byte, len(m.ResolvedAlerts)*10) var j5 int for _, num := range m.ResolvedAlerts { for num >= 1<<7 { dAtA6[j5] = uint8(uint64(num)&0x7f | 0x80) num >>= 7 j5++ } dAtA6[j5] = uint8(num) j5++ } dAtA[i] = 0x3a i++ i = encodeVarintNflog(dAtA, i, uint64(j5)) i += copy(dAtA[i:], dAtA6[:j5]) } return i, nil } func (m *MeshEntry) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalTo(dAtA) if err != nil { return nil, err } return dAtA[:n], nil } func (m *MeshEntry) MarshalTo(dAtA []byte) (int, error) { var i int _ = i var l int _ = l if m.Entry != nil { dAtA[i] = 0xa i++ i = encodeVarintNflog(dAtA, i, uint64(m.Entry.Size())) n7, err := m.Entry.MarshalTo(dAtA[i:]) if err != nil { return 0, err } i += n7 } dAtA[i] = 0x12 i++ i = encodeVarintNflog(dAtA, i, uint64(types.SizeOfStdTime(m.ExpiresAt))) n8, err := types.StdTimeMarshalTo(m.ExpiresAt, dAtA[i:]) if err != nil { return 0, err } i += n8 return i, nil } func encodeVarintNflog(dAtA []byte, offset int, v uint64) int { for v >= 1<<7 { dAtA[offset] = uint8(v&0x7f | 0x80) v >>= 7 offset++ } dAtA[offset] = uint8(v) return offset + 1 } func (m *Receiver) Size() (n int) { var l int _ = l l = len(m.GroupName) if l > 0 { n += 1 + l + sovNflog(uint64(l)) } l = len(m.Integration) if l > 0 { n += 1 + l + sovNflog(uint64(l)) } if m.Idx != 0 { n += 1 + sovNflog(uint64(m.Idx)) } return n } func (m *Entry) Size() (n int) { var l int _ = l l = len(m.GroupKey) if l > 0 { n += 1 + l + sovNflog(uint64(l)) } if m.Receiver != nil { l = m.Receiver.Size() n += 1 + l + sovNflog(uint64(l)) } l = len(m.GroupHash) if l > 0 { n += 1 + l + sovNflog(uint64(l)) } if m.Resolved { n += 2 } l = types.SizeOfStdTime(m.Timestamp) n += 1 + l + sovNflog(uint64(l)) if len(m.FiringAlerts) > 0 { l = 0 for _, e := range m.FiringAlerts { l += sovNflog(uint64(e)) } n += 1 + sovNflog(uint64(l)) + l } if len(m.ResolvedAlerts) > 0 { l = 0 for _, e := range m.ResolvedAlerts { l += sovNflog(uint64(e)) } n += 1 + sovNflog(uint64(l)) + l } return n } func (m *MeshEntry) Size() (n int) { var l int _ = l if m.Entry != nil { l = m.Entry.Size() n += 1 + l + sovNflog(uint64(l)) } l = types.SizeOfStdTime(m.ExpiresAt) n += 1 + l + sovNflog(uint64(l)) return n } func sovNflog(x uint64) (n int) { for { n++ x >>= 7 if x == 0 { break } } return n } func sozNflog(x uint64) (n int) { return sovNflog(uint64((x << 1) ^ uint64((int64(x) >> 63)))) } func (m *Receiver) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowNflog } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: Receiver: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: Receiver: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field GroupName", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowNflog } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ stringLen |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } intStringLen := int(stringLen) if intStringLen < 0 { return ErrInvalidLengthNflog } postIndex := iNdEx + intStringLen if postIndex > l { return io.ErrUnexpectedEOF } m.GroupName = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex case 2: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Integration", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowNflog } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ stringLen |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } intStringLen := int(stringLen) if intStringLen < 0 { return ErrInvalidLengthNflog } postIndex := iNdEx + intStringLen if postIndex > l { return io.ErrUnexpectedEOF } m.Integration = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex case 3: if wireType != 0 { return fmt.Errorf("proto: wrong wireType = %d for field Idx", wireType) } m.Idx = 0 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowNflog } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ m.Idx |= (uint32(b) & 0x7F) << shift if b < 0x80 { break } } default: iNdEx = preIndex skippy, err := skipNflog(dAtA[iNdEx:]) if err != nil { return err } if skippy < 0 { return ErrInvalidLengthNflog } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *Entry) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowNflog } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: Entry: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: Entry: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field GroupKey", wireType) } var byteLen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowNflog } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ byteLen |= (int(b) & 0x7F) << shift if b < 0x80 { break } } if byteLen < 0 { return ErrInvalidLengthNflog } postIndex := iNdEx + byteLen if postIndex > l { return io.ErrUnexpectedEOF } m.GroupKey = append(m.GroupKey[:0], dAtA[iNdEx:postIndex]...) if m.GroupKey == nil { m.GroupKey = []byte{} } iNdEx = postIndex case 2: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Receiver", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowNflog } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= (int(b) & 0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthNflog } postIndex := iNdEx + msglen if postIndex > l { return io.ErrUnexpectedEOF } if m.Receiver == nil { m.Receiver = &Receiver{} } if err := m.Receiver.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex case 3: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field GroupHash", wireType) } var byteLen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowNflog } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ byteLen |= (int(b) & 0x7F) << shift if b < 0x80 { break } } if byteLen < 0 { return ErrInvalidLengthNflog } postIndex := iNdEx + byteLen if postIndex > l { return io.ErrUnexpectedEOF } m.GroupHash = append(m.GroupHash[:0], dAtA[iNdEx:postIndex]...) if m.GroupHash == nil { m.GroupHash = []byte{} } iNdEx = postIndex case 4: if wireType != 0 { return fmt.Errorf("proto: wrong wireType = %d for field Resolved", wireType) } var v int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowNflog } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ v |= (int(b) & 0x7F) << shift if b < 0x80 { break } } m.Resolved = bool(v != 0) case 5: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Timestamp", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowNflog } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= (int(b) & 0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthNflog } postIndex := iNdEx + msglen if postIndex > l { return io.ErrUnexpectedEOF } if err := types.StdTimeUnmarshal(&m.Timestamp, dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex case 6: if wireType == 0 { var v uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowNflog } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ v |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } m.FiringAlerts = append(m.FiringAlerts, v) } else if wireType == 2 { var packedLen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowNflog } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ packedLen |= (int(b) & 0x7F) << shift if b < 0x80 { break } } if packedLen < 0 { return ErrInvalidLengthNflog } postIndex := iNdEx + packedLen if postIndex > l { return io.ErrUnexpectedEOF } for iNdEx < postIndex { var v uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowNflog } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ v |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } m.FiringAlerts = append(m.FiringAlerts, v) } } else { return fmt.Errorf("proto: wrong wireType = %d for field FiringAlerts", wireType) } case 7: if wireType == 0 { var v uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowNflog } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ v |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } m.ResolvedAlerts = append(m.ResolvedAlerts, v) } else if wireType == 2 { var packedLen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowNflog } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ packedLen |= (int(b) & 0x7F) << shift if b < 0x80 { break } } if packedLen < 0 { return ErrInvalidLengthNflog } postIndex := iNdEx + packedLen if postIndex > l { return io.ErrUnexpectedEOF } for iNdEx < postIndex { var v uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowNflog } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ v |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } m.ResolvedAlerts = append(m.ResolvedAlerts, v) } } else { return fmt.Errorf("proto: wrong wireType = %d for field ResolvedAlerts", wireType) } default: iNdEx = preIndex skippy, err := skipNflog(dAtA[iNdEx:]) if err != nil { return err } if skippy < 0 { return ErrInvalidLengthNflog } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *MeshEntry) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowNflog } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: MeshEntry: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: MeshEntry: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Entry", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowNflog } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= (int(b) & 0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthNflog } postIndex := iNdEx + msglen if postIndex > l { return io.ErrUnexpectedEOF } if m.Entry == nil { m.Entry = &Entry{} } if err := m.Entry.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex case 2: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field ExpiresAt", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowNflog } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= (int(b) & 0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthNflog } postIndex := iNdEx + msglen if postIndex > l { return io.ErrUnexpectedEOF } if err := types.StdTimeUnmarshal(&m.ExpiresAt, dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipNflog(dAtA[iNdEx:]) if err != nil { return err } if skippy < 0 { return ErrInvalidLengthNflog } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func skipNflog(dAtA []byte) (n int, err error) { l := len(dAtA) iNdEx := 0 for iNdEx < l { var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return 0, ErrIntOverflowNflog } if iNdEx >= l { return 0, io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } wireType := int(wire & 0x7) switch wireType { case 0: for shift := uint(0); ; shift += 7 { if shift >= 64 { return 0, ErrIntOverflowNflog } if iNdEx >= l { return 0, io.ErrUnexpectedEOF } iNdEx++ if dAtA[iNdEx-1] < 0x80 { break } } return iNdEx, nil case 1: iNdEx += 8 return iNdEx, nil case 2: var length int for shift := uint(0); ; shift += 7 { if shift >= 64 { return 0, ErrIntOverflowNflog } if iNdEx >= l { return 0, io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ length |= (int(b) & 0x7F) << shift if b < 0x80 { break } } iNdEx += length if length < 0 { return 0, ErrInvalidLengthNflog } return iNdEx, nil case 3: for { var innerWire uint64 var start int = iNdEx for shift := uint(0); ; shift += 7 { if shift >= 64 { return 0, ErrIntOverflowNflog } if iNdEx >= l { return 0, io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ innerWire |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } innerWireType := int(innerWire & 0x7) if innerWireType == 4 { break } next, err := skipNflog(dAtA[start:]) if err != nil { return 0, err } iNdEx = start + next } return iNdEx, nil case 4: return iNdEx, nil case 5: iNdEx += 4 return iNdEx, nil default: return 0, fmt.Errorf("proto: illegal wireType %d", wireType) } } panic("unreachable") } var ( ErrInvalidLengthNflog = fmt.Errorf("proto: negative length found during unmarshaling") ErrIntOverflowNflog = fmt.Errorf("proto: integer overflow") ) func init() { proto.RegisterFile("nflog.proto", fileDescriptorNflog) } var fileDescriptorNflog = []byte{ // 385 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x90, 0xcf, 0x6e, 0xd3, 0x40, 0x10, 0xc6, 0xbb, 0x4d, 0xd3, 0xda, 0xe3, 0xb4, 0x94, 0x15, 0x07, 0xcb, 0x08, 0xc7, 0x0a, 0x48, 0xf8, 0x82, 0x23, 0x95, 0x27, 0x68, 0x10, 0x12, 0x12, 0x82, 0xc3, 0x8a, 0x2b, 0xb2, 0x36, 0x74, 0xb2, 0x5e, 0x61, 0x7b, 0xad, 0xf5, 0x36, 0x6a, 0xde, 0x82, 0x47, 0xe0, 0x71, 0x72, 0xe4, 0x09, 0xf8, 0x93, 0x27, 0x41, 0xde, 0xb5, 0x1d, 0x8e, 0xdc, 0x66, 0x7f, 0xf3, 0xcd, 0xcc, 0xb7, 0x1f, 0x04, 0xf5, 0xa6, 0x54, 0x22, 0x6b, 0xb4, 0x32, 0x8a, 0x5e, 0xd8, 0x47, 0xb3, 0x8e, 0xe6, 0x42, 0x29, 0x51, 0xe2, 0xd2, 0xe2, 0xf5, 0xfd, 0x66, 0x69, 0x64, 0x85, 0xad, 0xe1, 0x55, 0xe3, 0x94, 0xd1, 0x13, 0xa1, 0x84, 0xb2, 0xe5, 0xb2, 0xab, 0x1c, 0x5d, 0x7c, 0x06, 0x8f, 0xe1, 0x17, 0x94, 0x5b, 0xd4, 0xf4, 0x19, 0x80, 0xd0, 0xea, 0xbe, 0xc9, 0x6b, 0x5e, 0x61, 0x48, 0x12, 0x92, 0xfa, 0xcc, 0xb7, 0xe4, 0x23, 0xaf, 0x90, 0x26, 0x10, 0xc8, 0xda, 0xa0, 0xd0, 0xdc, 0x48, 0x55, 0x87, 0xa7, 0xb6, 0xff, 0x2f, 0xa2, 0xd7, 0x30, 0x91, 0x77, 0x0f, 0xe1, 0x24, 0x21, 0xe9, 0x25, 0xeb, 0xca, 0xc5, 0xf7, 0x53, 0x98, 0xbe, 0xad, 0x8d, 0xde, 0xd1, 0xa7, 0xe0, 0x56, 0xe5, 0x5f, 0x71, 0x67, 0x77, 0xcf, 0x98, 0x67, 0xc1, 0x7b, 0xdc, 0xd1, 0x57, 0xe0, 0xe9, 0xde, 0x85, 0xdd, 0x1b, 0xdc, 0x3c, 0xce, 0xfa, 0x8f, 0x65, 0x83, 0x3d, 0x36, 0x4a, 0x8e, 0x46, 0x0b, 0xde, 0x16, 0xf6, 0xdc, 0xac, 0x37, 0xfa, 0x8e, 0xb7, 0x05, 0x8d, 0xba, 0x6d, 0xad, 0x2a, 0xb7, 0x78, 0x17, 0x9e, 0x25, 0x24, 0xf5, 0xd8, 0xf8, 0xa6, 0x2b, 0xf0, 0xc7, 0x60, 0xc2, 0xa9, 0x3d, 0x15, 0x65, 0x2e, 0xba, 0x6c, 0x88, 0x2e, 0xfb, 0x34, 0x28, 0x56, 0xde, 0xfe, 0xe7, 0xfc, 0xe4, 0xdb, 0xaf, 0x39, 0x61, 0xc7, 0x31, 0xfa, 0x1c, 0x2e, 0x37, 0x52, 0xcb, 0x5a, 0xe4, 0xbc, 0x44, 0x6d, 0xda, 0xf0, 0x3c, 0x99, 0xa4, 0x67, 0x6c, 0xe6, 0xe0, 0xad, 0x65, 0xf4, 0x25, 0x3c, 0x1a, 0x8e, 0x0e, 0xb2, 0x0b, 0x2b, 0xbb, 0x1a, 0xb0, 0x13, 0x2e, 0xb6, 0xe0, 0x7f, 0xc0, 0xb6, 0x70, 0x29, 0xbd, 0x80, 0x29, 0x76, 0x85, 0x4d, 0x28, 0xb8, 0xb9, 0x1a, 0x53, 0xb0, 0x6d, 0xe6, 0x9a, 0xf4, 0x0d, 0x00, 0x3e, 0x34, 0x52, 0x63, 0x9b, 0x73, 0xd3, 0x07, 0xf6, 0x9f, 0xbf, 0xe8, 0xe7, 0x6e, 0xcd, 0xea, 0x7a, 0xff, 0x27, 0x3e, 0xd9, 0x1f, 0x62, 0xf2, 0xe3, 0x10, 0x93, 0xdf, 0x87, 0x98, 0xac, 0xcf, 0xed, 0xe8, 0xeb, 0xbf, 0x01, 0x00, 0x00, 0xff, 0xff, 0x49, 0xcd, 0xa7, 0x1e, 0x61, 0x02, 0x00, 0x00, } prometheus-alertmanager-0.15.3+ds/nflog/nflogpb/nflog.proto000066400000000000000000000035571341674552200240060ustar00rootroot00000000000000syntax = "proto3"; package nflogpb; import "google/protobuf/timestamp.proto"; import "gogoproto/gogo.proto"; option (gogoproto.marshaler_all) = true; option (gogoproto.sizer_all) = true; option (gogoproto.unmarshaler_all) = true; option (gogoproto.goproto_getters_all) = false; message Receiver { // Configured name of the receiver group. string group_name = 1; // Name of the integration of the receiver. string integration = 2; // Index of the receiver with respect to the integration. // Every integration in a group may have 0..N configurations. uint32 idx = 3; } // Entry holds information about a successful notification // sent to a receiver. message Entry { // The key identifying the dispatching group. bytes group_key = 1; // The receiver that was notified. Receiver receiver = 2; // Hash over the state of the group at notification time. // Deprecated in favor of FiringAlerts field, but kept for compatibility. bytes group_hash = 3; // Whether the notification was about a resolved alert. // Deprecated in favor of ResolvedAlerts field, but kept for compatibility. bool resolved = 4; // Timestamp of the succeeding notification. google.protobuf.Timestamp timestamp = 5 [(gogoproto.stdtime) = true, (gogoproto.nullable) = false]; // FiringAlerts list of hashes of firing alerts at the last notification time. repeated uint64 firing_alerts = 6; // ResolvedAlerts list of hashes of resolved alerts at the last notification time. repeated uint64 resolved_alerts = 7; } // MeshEntry is a wrapper message to communicate a notify log // entry through a mesh network. message MeshEntry { // The original raw notify log entry. Entry entry = 1; // A timestamp indicating when the mesh peer should evict // the log entry from its state. google.protobuf.Timestamp expires_at = 2 [(gogoproto.stdtime) = true, (gogoproto.nullable) = false]; } prometheus-alertmanager-0.15.3+ds/nflog/nflogpb/set.go000066400000000000000000000026461341674552200227340ustar00rootroot00000000000000// Copyright 2017 Prometheus Team // 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 nflogpb // IsFiringSubset returns whether the given subset is a subset of the alerts // that were firing at the time of the last notification. func (m *Entry) IsFiringSubset(subset map[uint64]struct{}) bool { set := map[uint64]struct{}{} for i := range m.FiringAlerts { set[m.FiringAlerts[i]] = struct{}{} } return isSubset(set, subset) } // IsResolvedSubset returns whether the given subset is a subset of the alerts // that were resolved at the time of the last notification. func (m *Entry) IsResolvedSubset(subset map[uint64]struct{}) bool { set := map[uint64]struct{}{} for i := range m.ResolvedAlerts { set[m.ResolvedAlerts[i]] = struct{}{} } return isSubset(set, subset) } func isSubset(set, subset map[uint64]struct{}) bool { for k := range subset { _, exists := set[k] if !exists { return false } } return true } prometheus-alertmanager-0.15.3+ds/nflog/nflogpb/set_test.go000066400000000000000000000043311341674552200237640ustar00rootroot00000000000000// Copyright 2018 Prometheus Team // 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 nflogpb import ( "testing" ) func TestIsFiringSubset(t *testing.T) { e := &Entry{ FiringAlerts: []uint64{1, 2, 3}, } tests := []struct { subset map[uint64]struct{} expected bool }{ {newSubset(), true}, //empty subset {newSubset(1), true}, {newSubset(2), true}, {newSubset(3), true}, {newSubset(1, 2), true}, {newSubset(1, 2), true}, {newSubset(1, 2, 3), true}, {newSubset(4), false}, {newSubset(1, 5), false}, {newSubset(1, 2, 3, 6), false}, } for _, test := range tests { if result := e.IsFiringSubset(test.subset); result != test.expected { t.Errorf("Expected %t, got %t for subset %v", test.expected, result, elements(test.subset)) } } } func TestIsResolvedSubset(t *testing.T) { e := &Entry{ ResolvedAlerts: []uint64{1, 2, 3}, } tests := []struct { subset map[uint64]struct{} expected bool }{ {newSubset(), true}, //empty subset {newSubset(1), true}, {newSubset(2), true}, {newSubset(3), true}, {newSubset(1, 2), true}, {newSubset(1, 2), true}, {newSubset(1, 2, 3), true}, {newSubset(4), false}, {newSubset(1, 5), false}, {newSubset(1, 2, 3, 6), false}, } for _, test := range tests { if result := e.IsResolvedSubset(test.subset); result != test.expected { t.Errorf("Expected %t, got %t for subset %v", test.expected, result, elements(test.subset)) } } } func newSubset(elements ...uint64) map[uint64]struct{} { subset := make(map[uint64]struct{}) for _, el := range elements { subset[el] = struct{}{} } return subset } func elements(m map[uint64]struct{}) []uint64 { els := make([]uint64, 0, len(m)) for k, _ := range m { els = append(els, k) } return els } prometheus-alertmanager-0.15.3+ds/notify/000077500000000000000000000000001341674552200203565ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/notify/impl.go000066400000000000000000001172431341674552200216560ustar00rootroot00000000000000// Copyright 2015 Prometheus Team // 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 notify import ( "bytes" "crypto/sha256" "crypto/tls" "encoding/json" "errors" "fmt" "io/ioutil" "mime" "mime/multipart" "net" "net/http" "net/mail" "net/smtp" "net/textproto" "net/url" "strings" "time" "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" commoncfg "github.com/prometheus/common/config" "github.com/prometheus/common/model" "github.com/prometheus/common/version" "golang.org/x/net/context" "golang.org/x/net/context/ctxhttp" "github.com/prometheus/alertmanager/config" "github.com/prometheus/alertmanager/template" "github.com/prometheus/alertmanager/types" ) // A Notifier notifies about alerts under constraints of the given context. // It returns an error if unsuccessful and a flag whether the error is // recoverable. This information is useful for a retry logic. type Notifier interface { Notify(context.Context, ...*types.Alert) (bool, error) } // An Integration wraps a notifier and its config to be uniquely identified by // name and index from its origin in the configuration. type Integration struct { notifier Notifier conf notifierConfig name string idx int } // Notify implements the Notifier interface. func (i *Integration) Notify(ctx context.Context, alerts ...*types.Alert) (bool, error) { return i.notifier.Notify(ctx, alerts...) } // BuildReceiverIntegrations builds a list of integration notifiers off of a // receivers config. func BuildReceiverIntegrations(nc *config.Receiver, tmpl *template.Template, logger log.Logger) []Integration { var ( integrations []Integration add = func(name string, i int, n Notifier, nc notifierConfig) { integrations = append(integrations, Integration{ notifier: n, conf: nc, name: name, idx: i, }) } ) for i, c := range nc.WebhookConfigs { n := NewWebhook(c, tmpl, logger) add("webhook", i, n, c) } for i, c := range nc.EmailConfigs { n := NewEmail(c, tmpl, logger) add("email", i, n, c) } for i, c := range nc.PagerdutyConfigs { n := NewPagerDuty(c, tmpl, logger) add("pagerduty", i, n, c) } for i, c := range nc.OpsGenieConfigs { n := NewOpsGenie(c, tmpl, logger) add("opsgenie", i, n, c) } for i, c := range nc.WechatConfigs { n := NewWechat(c, tmpl, logger) add("wechat", i, n, c) } for i, c := range nc.SlackConfigs { n := NewSlack(c, tmpl, logger) add("slack", i, n, c) } for i, c := range nc.HipchatConfigs { n := NewHipchat(c, tmpl, logger) add("hipchat", i, n, c) } for i, c := range nc.VictorOpsConfigs { n := NewVictorOps(c, tmpl, logger) add("victorops", i, n, c) } for i, c := range nc.PushoverConfigs { n := NewPushover(c, tmpl, logger) add("pushover", i, n, c) } return integrations } const contentTypeJSON = "application/json" var userAgentHeader = fmt.Sprintf("Alertmanager/%s", version.Version) // Webhook implements a Notifier for generic webhooks. type Webhook struct { conf *config.WebhookConfig tmpl *template.Template logger log.Logger } // NewWebhook returns a new Webhook. func NewWebhook(conf *config.WebhookConfig, t *template.Template, l log.Logger) *Webhook { return &Webhook{conf: conf, tmpl: t, logger: l} } // WebhookMessage defines the JSON object send to webhook endpoints. type WebhookMessage struct { *template.Data // The protocol version. Version string `json:"version"` GroupKey string `json:"groupKey"` } // Notify implements the Notifier interface. func (w *Webhook) Notify(ctx context.Context, alerts ...*types.Alert) (bool, error) { data := w.tmpl.Data(receiverName(ctx, w.logger), groupLabels(ctx, w.logger), alerts...) groupKey, ok := GroupKey(ctx) if !ok { level.Error(w.logger).Log("msg", "group key missing") } msg := &WebhookMessage{ Version: "4", Data: data, GroupKey: groupKey, } var buf bytes.Buffer if err := json.NewEncoder(&buf).Encode(msg); err != nil { return false, err } req, err := http.NewRequest("POST", w.conf.URL.String(), &buf) if err != nil { return true, err } req.Header.Set("Content-Type", contentTypeJSON) req.Header.Set("User-Agent", userAgentHeader) c, err := commoncfg.NewClientFromConfig(*w.conf.HTTPConfig, "webhook") if err != nil { return false, err } resp, err := ctxhttp.Do(ctx, c, req) if err != nil { return true, err } resp.Body.Close() return w.retry(resp.StatusCode) } func (w *Webhook) retry(statusCode int) (bool, error) { // Webhooks are assumed to respond with 2xx response codes on a successful // request and 5xx response codes are assumed to be recoverable. if statusCode/100 != 2 { return (statusCode/100 == 5), fmt.Errorf("unexpected status code %v from %s", statusCode, w.conf.URL) } return false, nil } // Email implements a Notifier for email notifications. type Email struct { conf *config.EmailConfig tmpl *template.Template logger log.Logger } // NewEmail returns a new Email notifier. func NewEmail(c *config.EmailConfig, t *template.Template, l log.Logger) *Email { if _, ok := c.Headers["Subject"]; !ok { c.Headers["Subject"] = config.DefaultEmailSubject } if _, ok := c.Headers["To"]; !ok { c.Headers["To"] = c.To } if _, ok := c.Headers["From"]; !ok { c.Headers["From"] = c.From } return &Email{conf: c, tmpl: t, logger: l} } // auth resolves a string of authentication mechanisms. func (n *Email) auth(mechs string) (smtp.Auth, error) { username := n.conf.AuthUsername for _, mech := range strings.Split(mechs, " ") { switch mech { case "CRAM-MD5": secret := string(n.conf.AuthSecret) if secret == "" { continue } return smtp.CRAMMD5Auth(username, secret), nil case "PLAIN": password := string(n.conf.AuthPassword) if password == "" { continue } identity := n.conf.AuthIdentity // We need to know the hostname for both auth and TLS. host, _, err := net.SplitHostPort(n.conf.Smarthost) if err != nil { return nil, fmt.Errorf("invalid address: %s", err) } return smtp.PlainAuth(identity, username, password, host), nil case "LOGIN": password := string(n.conf.AuthPassword) if password == "" { continue } return LoginAuth(username, password), nil } } return nil, nil } // Notify implements the Notifier interface. func (n *Email) Notify(ctx context.Context, as ...*types.Alert) (bool, error) { // We need to know the hostname for both auth and TLS. var c *smtp.Client host, port, err := net.SplitHostPort(n.conf.Smarthost) if err != nil { return false, fmt.Errorf("invalid address: %s", err) } if port == "465" { conn, err := tls.Dial("tcp", n.conf.Smarthost, &tls.Config{ServerName: host}) if err != nil { return true, err } c, err = smtp.NewClient(conn, n.conf.Smarthost) if err != nil { return true, err } } else { // Connect to the SMTP smarthost. c, err = smtp.Dial(n.conf.Smarthost) if err != nil { return true, err } } defer c.Quit() if n.conf.Hello != "" { err := c.Hello(n.conf.Hello) if err != nil { return true, err } } // Global Config guarantees RequireTLS is not nil if *n.conf.RequireTLS { if ok, _ := c.Extension("STARTTLS"); !ok { return true, fmt.Errorf("require_tls: true (default), but %q does not advertise the STARTTLS extension", n.conf.Smarthost) } tlsConf := &tls.Config{ServerName: host} if err := c.StartTLS(tlsConf); err != nil { return true, fmt.Errorf("starttls failed: %s", err) } } if ok, mech := c.Extension("AUTH"); ok { auth, err := n.auth(mech) if err != nil { return true, err } if auth != nil { if err := c.Auth(auth); err != nil { return true, fmt.Errorf("%T failed: %s", auth, err) } } } var ( tmplErr error data = n.tmpl.Data(receiverName(ctx, n.logger), groupLabels(ctx, n.logger), as...) tmpl = tmplText(n.tmpl, data, &tmplErr) from = tmpl(n.conf.From) to = tmpl(n.conf.To) ) if tmplErr != nil { return false, fmt.Errorf("failed to template 'from' or 'to': %v", tmplErr) } addrs, err := mail.ParseAddressList(from) if err != nil { return false, fmt.Errorf("parsing from addresses: %s", err) } if len(addrs) != 1 { return false, fmt.Errorf("must be exactly one from address") } if err := c.Mail(addrs[0].Address); err != nil { return true, fmt.Errorf("sending mail from: %s", err) } addrs, err = mail.ParseAddressList(to) if err != nil { return false, fmt.Errorf("parsing to addresses: %s", err) } for _, addr := range addrs { if err := c.Rcpt(addr.Address); err != nil { return true, fmt.Errorf("sending rcpt to: %s", err) } } // Send the email body. wc, err := c.Data() if err != nil { return true, err } defer wc.Close() for header, t := range n.conf.Headers { value, err := n.tmpl.ExecuteTextString(t, data) if err != nil { return false, fmt.Errorf("executing %q header template: %s", header, err) } fmt.Fprintf(wc, "%s: %s\r\n", header, mime.QEncoding.Encode("utf-8", value)) } buffer := &bytes.Buffer{} multipartWriter := multipart.NewWriter(buffer) fmt.Fprintf(wc, "Date: %s\r\n", time.Now().Format(time.RFC1123Z)) fmt.Fprintf(wc, "Content-Type: multipart/alternative; boundary=%s\r\n", multipartWriter.Boundary()) fmt.Fprintf(wc, "MIME-Version: 1.0\r\n") // TODO: Add some useful headers here, such as URL of the alertmanager // and active/resolved. fmt.Fprintf(wc, "\r\n") if len(n.conf.Text) > 0 { // Text template w, err := multipartWriter.CreatePart(textproto.MIMEHeader{"Content-Type": {"text/plain; charset=UTF-8"}}) if err != nil { return false, fmt.Errorf("creating part for text template: %s", err) } body, err := n.tmpl.ExecuteTextString(n.conf.Text, data) if err != nil { return false, fmt.Errorf("executing email text template: %s", err) } _, err = w.Write([]byte(body)) if err != nil { return true, err } } if len(n.conf.HTML) > 0 { // Html template // Preferred alternative placed last per section 5.1.4 of RFC 2046 // https://www.ietf.org/rfc/rfc2046.txt w, err := multipartWriter.CreatePart(textproto.MIMEHeader{"Content-Type": {"text/html; charset=UTF-8"}}) if err != nil { return false, fmt.Errorf("creating part for html template: %s", err) } body, err := n.tmpl.ExecuteHTMLString(n.conf.HTML, data) if err != nil { return false, fmt.Errorf("executing email html template: %s", err) } _, err = w.Write([]byte(body)) if err != nil { return true, err } } err = multipartWriter.Close() if err != nil { return false, fmt.Errorf("failed to close multipartWriter: %v", err) } _, err = wc.Write(buffer.Bytes()) if err != nil { return false, fmt.Errorf("failed to write body buffer: %v", err) } return false, nil } // PagerDuty implements a Notifier for PagerDuty notifications. type PagerDuty struct { conf *config.PagerdutyConfig tmpl *template.Template logger log.Logger } // NewPagerDuty returns a new PagerDuty notifier. func NewPagerDuty(c *config.PagerdutyConfig, t *template.Template, l log.Logger) *PagerDuty { return &PagerDuty{conf: c, tmpl: t, logger: l} } const ( pagerDutyEventTrigger = "trigger" pagerDutyEventResolve = "resolve" ) type pagerDutyMessage struct { RoutingKey string `json:"routing_key,omitempty"` ServiceKey string `json:"service_key,omitempty"` DedupKey string `json:"dedup_key,omitempty"` IncidentKey string `json:"incident_key,omitempty"` EventType string `json:"event_type,omitempty"` Description string `json:"description,omitempty"` EventAction string `json:"event_action"` Payload *pagerDutyPayload `json:"payload"` Client string `json:"client,omitempty"` ClientURL string `json:"client_url,omitempty"` Details map[string]string `json:"details,omitempty"` } type pagerDutyPayload struct { Summary string `json:"summary"` Source string `json:"source"` Severity string `json:"severity"` Timestamp string `json:"timestamp,omitempty"` Class string `json:"class,omitempty"` Component string `json:"component,omitempty"` Group string `json:"group,omitempty"` CustomDetails map[string]string `json:"custom_details,omitempty"` } func (n *PagerDuty) notifyV1( ctx context.Context, c *http.Client, eventType, key string, data *template.Data, details map[string]string, as ...*types.Alert, ) (bool, error) { var tmplErr error tmpl := tmplText(n.tmpl, data, &tmplErr) msg := &pagerDutyMessage{ ServiceKey: tmpl(string(n.conf.ServiceKey)), EventType: eventType, IncidentKey: hashKey(key), Description: tmpl(n.conf.Description), Details: details, } apiURL, err := url.Parse("https://events.pagerduty.com/generic/2010-04-15/create_event.json") if err != nil { return false, err } n.conf.URL = &config.URL{apiURL} if eventType == pagerDutyEventTrigger { msg.Client = tmpl(n.conf.Client) msg.ClientURL = tmpl(n.conf.ClientURL) } if tmplErr != nil { return false, fmt.Errorf("failed to template PagerDuty v1 message: %v", tmplErr) } var buf bytes.Buffer if err := json.NewEncoder(&buf).Encode(msg); err != nil { return false, err } resp, err := ctxhttp.Post(ctx, c, n.conf.URL.String(), contentTypeJSON, &buf) if err != nil { return true, err } defer resp.Body.Close() return n.retryV1(resp) } func (n *PagerDuty) notifyV2( ctx context.Context, c *http.Client, eventType, key string, data *template.Data, details map[string]string, as ...*types.Alert, ) (bool, error) { var tmplErr error tmpl := tmplText(n.tmpl, data, &tmplErr) if n.conf.Severity == "" { n.conf.Severity = "error" } msg := &pagerDutyMessage{ Client: tmpl(n.conf.Client), ClientURL: tmpl(n.conf.ClientURL), RoutingKey: tmpl(string(n.conf.RoutingKey)), EventAction: eventType, DedupKey: hashKey(key), Payload: &pagerDutyPayload{ Summary: tmpl(n.conf.Description), Source: tmpl(n.conf.Client), Severity: tmpl(n.conf.Severity), CustomDetails: details, Class: tmpl(n.conf.Class), Component: tmpl(n.conf.Component), Group: tmpl(n.conf.Group), }, } if tmplErr != nil { return false, fmt.Errorf("failed to template PagerDuty v2 message: %v", tmplErr) } var buf bytes.Buffer if err := json.NewEncoder(&buf).Encode(msg); err != nil { return false, err } resp, err := ctxhttp.Post(ctx, c, n.conf.URL.String(), contentTypeJSON, &buf) if err != nil { return true, err } defer resp.Body.Close() return n.retryV2(resp.StatusCode) } // Notify implements the Notifier interface. // // https://v2.developer.pagerduty.com/docs/events-api-v2 func (n *PagerDuty) Notify(ctx context.Context, as ...*types.Alert) (bool, error) { key, ok := GroupKey(ctx) if !ok { return false, fmt.Errorf("group key missing") } var err error var ( alerts = types.Alerts(as...) data = n.tmpl.Data(receiverName(ctx, n.logger), groupLabels(ctx, n.logger), as...) eventType = pagerDutyEventTrigger ) if alerts.Status() == model.AlertResolved { eventType = pagerDutyEventResolve } level.Debug(n.logger).Log("msg", "Notifying PagerDuty", "incident", key, "eventType", eventType) details := make(map[string]string, len(n.conf.Details)) for k, v := range n.conf.Details { detail, err := n.tmpl.ExecuteTextString(v, data) if err != nil { return false, fmt.Errorf("failed to template %q: %v", v, err) } details[k] = detail } if err != nil { return false, err } c, err := commoncfg.NewClientFromConfig(*n.conf.HTTPConfig, "pagerduty") if err != nil { return false, err } if n.conf.ServiceKey != "" { return n.notifyV1(ctx, c, eventType, key, data, details, as...) } return n.notifyV2(ctx, c, eventType, key, data, details, as...) } func (n *PagerDuty) retryV1(resp *http.Response) (bool, error) { // Retrying can solve the issue on 403 (rate limiting) and 5xx response codes. // 2xx response codes indicate a successful request. // https://v2.developer.pagerduty.com/docs/trigger-events statusCode := resp.StatusCode if statusCode == 400 && resp.Body != nil { bs, err := ioutil.ReadAll(resp.Body) if err != nil { return false, fmt.Errorf("unexpected status code %v : problem reading response: %v", statusCode, err) } return false, fmt.Errorf("bad request (status code %v): %v", statusCode, string(bs)) } if statusCode/100 != 2 { return (statusCode == 403 || statusCode/100 == 5), fmt.Errorf("unexpected status code %v", statusCode) } return false, nil } func (n *PagerDuty) retryV2(statusCode int) (bool, error) { // Retrying can solve the issue on 429 (rate limiting) and 5xx response codes. // 2xx response codes indicate a successful request. // https://v2.developer.pagerduty.com/docs/events-api-v2#api-response-codes--retry-logic if statusCode/100 != 2 { return (statusCode == 429 || statusCode/100 == 5), fmt.Errorf("unexpected status code %v", statusCode) } return false, nil } // Slack implements a Notifier for Slack notifications. type Slack struct { conf *config.SlackConfig tmpl *template.Template logger log.Logger } // NewSlack returns a new Slack notification handler. func NewSlack(c *config.SlackConfig, t *template.Template, l log.Logger) *Slack { return &Slack{ conf: c, tmpl: t, logger: l, } } // slackReq is the request for sending a slack notification. type slackReq struct { Channel string `json:"channel,omitempty"` Username string `json:"username,omitempty"` IconEmoji string `json:"icon_emoji,omitempty"` IconURL string `json:"icon_url,omitempty"` LinkNames bool `json:"link_names,omitempty"` Attachments []slackAttachment `json:"attachments"` } // slackAttachment is used to display a richly-formatted message block. type slackAttachment struct { Title string `json:"title,omitempty"` TitleLink string `json:"title_link,omitempty"` Pretext string `json:"pretext,omitempty"` Text string `json:"text"` Fallback string `json:"fallback"` Fields []config.SlackField `json:"fields,omitempty"` Actions []config.SlackAction `json:"actions,omitempty"` Footer string `json:"footer"` Color string `json:"color,omitempty"` MrkdwnIn []string `json:"mrkdwn_in,omitempty"` } // Notify implements the Notifier interface. func (n *Slack) Notify(ctx context.Context, as ...*types.Alert) (bool, error) { var err error var ( data = n.tmpl.Data(receiverName(ctx, n.logger), groupLabels(ctx, n.logger), as...) tmplText = tmplText(n.tmpl, data, &err) ) attachment := &slackAttachment{ Title: tmplText(n.conf.Title), TitleLink: tmplText(n.conf.TitleLink), Pretext: tmplText(n.conf.Pretext), Text: tmplText(n.conf.Text), Fallback: tmplText(n.conf.Fallback), Footer: tmplText(n.conf.Footer), Color: tmplText(n.conf.Color), MrkdwnIn: []string{"fallback", "pretext", "text"}, } var numFields = len(n.conf.Fields) if numFields > 0 { var fields = make([]config.SlackField, numFields) for index, field := range n.conf.Fields { // Check if short was defined for the field otherwise fallback to the global setting var short bool if field.Short != nil { short = *field.Short } else { short = n.conf.ShortFields } // Rebuild the field by executing any templates and setting the new value for short fields[index] = config.SlackField{ Title: tmplText(field.Title), Value: tmplText(field.Value), Short: &short, } } attachment.Fields = fields } var numActions = len(n.conf.Actions) if numActions > 0 { var actions = make([]config.SlackAction, numActions) for index, action := range n.conf.Actions { actions[index] = config.SlackAction{ Type: tmplText(action.Type), Text: tmplText(action.Text), URL: tmplText(action.URL), Style: tmplText(action.Style), } } attachment.Actions = actions } req := &slackReq{ Channel: tmplText(n.conf.Channel), Username: tmplText(n.conf.Username), IconEmoji: tmplText(n.conf.IconEmoji), IconURL: tmplText(n.conf.IconURL), LinkNames: n.conf.LinkNames, Attachments: []slackAttachment{*attachment}, } if err != nil { return false, err } var buf bytes.Buffer if err := json.NewEncoder(&buf).Encode(req); err != nil { return false, err } c, err := commoncfg.NewClientFromConfig(*n.conf.HTTPConfig, "slack") if err != nil { return false, err } resp, err := ctxhttp.Post(ctx, c, n.conf.APIURL.String(), contentTypeJSON, &buf) if err != nil { return true, err } resp.Body.Close() return n.retry(resp.StatusCode) } func (n *Slack) retry(statusCode int) (bool, error) { // Only 5xx response codes are recoverable and 2xx codes are successful. // https://api.slack.com/incoming-webhooks#handling_errors // https://api.slack.com/changelog/2016-05-17-changes-to-errors-for-incoming-webhooks if statusCode/100 != 2 { return (statusCode/100 == 5), fmt.Errorf("unexpected status code %v", statusCode) } return false, nil } // Hipchat implements a Notifier for Hipchat notifications. type Hipchat struct { conf *config.HipchatConfig tmpl *template.Template logger log.Logger } // NewHipchat returns a new Hipchat notification handler. func NewHipchat(c *config.HipchatConfig, t *template.Template, l log.Logger) *Hipchat { return &Hipchat{ conf: c, tmpl: t, logger: l, } } type hipchatReq struct { From string `json:"from"` Notify bool `json:"notify"` Message string `json:"message"` MessageFormat string `json:"message_format"` Color string `json:"color"` } // Notify implements the Notifier interface. func (n *Hipchat) Notify(ctx context.Context, as ...*types.Alert) (bool, error) { var err error var msg string var ( data = n.tmpl.Data(receiverName(ctx, n.logger), groupLabels(ctx, n.logger), as...) tmplText = tmplText(n.tmpl, data, &err) tmplHTML = tmplHTML(n.tmpl, data, &err) roomid = tmplText(n.conf.RoomID) apiURL = n.conf.APIURL.Copy() ) apiURL.Path += fmt.Sprintf("v2/room/%s/notification", roomid) q := apiURL.Query() q.Set("auth_token", fmt.Sprintf("%s", n.conf.AuthToken)) apiURL.RawQuery = q.Encode() if n.conf.MessageFormat == "html" { msg = tmplHTML(n.conf.Message) } else { msg = tmplText(n.conf.Message) } req := &hipchatReq{ From: tmplText(n.conf.From), Notify: n.conf.Notify, Message: msg, MessageFormat: n.conf.MessageFormat, Color: tmplText(n.conf.Color), } if err != nil { return false, err } var buf bytes.Buffer if err := json.NewEncoder(&buf).Encode(req); err != nil { return false, err } c, err := commoncfg.NewClientFromConfig(*n.conf.HTTPConfig, "hipchat") if err != nil { return false, err } resp, err := ctxhttp.Post(ctx, c, apiURL.String(), contentTypeJSON, &buf) if err != nil { return true, err } defer resp.Body.Close() return n.retry(resp.StatusCode) } func (n *Hipchat) retry(statusCode int) (bool, error) { // Response codes 429 (rate limiting) and 5xx can potentially recover. 2xx // responce codes indicate successful requests. // https://developer.atlassian.com/hipchat/guide/hipchat-rest-api/api-response-codes if statusCode/100 != 2 { return (statusCode == 429 || statusCode/100 == 5), fmt.Errorf("unexpected status code %v", statusCode) } return false, nil } // Wechat implements a Notfier for wechat notifications type Wechat struct { conf *config.WechatConfig tmpl *template.Template logger log.Logger accessToken string accessTokenAt time.Time } // Wechat AccessToken with corpid and corpsecret. type WechatToken struct { AccessToken string `json:"access_token"` } type weChatMessage struct { Text weChatMessageContent `yaml:"text,omitempty" json:"text,omitempty"` ToUser string `yaml:"touser,omitempty" json:"touser,omitempty"` ToParty string `yaml:"toparty,omitempty" json:"toparty,omitempty"` Totag string `yaml:"totag,omitempty" json:"totag,omitempty"` AgentID string `yaml:"agentid,omitempty" json:"agentid,omitempty"` Safe string `yaml:"safe,omitempty" json:"safe,omitempty"` Type string `yaml:"msgtype,omitempty" json:"msgtype,omitempty"` } type weChatMessageContent struct { Content string `json:"content"` } type weChatResponse struct { Code int `json:"code"` Error string `json:"error"` } // NewWechat returns a new Wechat notifier. func NewWechat(c *config.WechatConfig, t *template.Template, l log.Logger) *Wechat { return &Wechat{conf: c, tmpl: t, logger: l} } // Notify implements the Notifier interface. func (n *Wechat) Notify(ctx context.Context, as ...*types.Alert) (bool, error) { key, ok := GroupKey(ctx) if !ok { return false, fmt.Errorf("group key missing") } level.Debug(n.logger).Log("msg", "Notifying Wechat", "incident", key) data := n.tmpl.Data(receiverName(ctx, n.logger), groupLabels(ctx, n.logger), as...) var err error tmpl := tmplText(n.tmpl, data, &err) if err != nil { return false, err } c, err := commoncfg.NewClientFromConfig(*n.conf.HTTPConfig, "wechat") if err != nil { return false, err } // Refresh AccessToken over 2 hours if n.accessToken == "" || time.Now().Sub(n.accessTokenAt) > 2*time.Hour { parameters := url.Values{} parameters.Add("corpsecret", tmpl(string(n.conf.APISecret))) parameters.Add("corpid", tmpl(string(n.conf.CorpID))) if err != nil { return false, fmt.Errorf("templating error: %s", err) } u := n.conf.APIURL.Copy() u.Path += "gettoken" u.RawQuery = parameters.Encode() req, err := http.NewRequest(http.MethodGet, u.String(), nil) if err != nil { return true, err } req.Header.Set("Content-Type", contentTypeJSON) resp, err := c.Do(req.WithContext(ctx)) if err != nil { return true, err } defer resp.Body.Close() var wechatToken WechatToken if err := json.NewDecoder(resp.Body).Decode(&wechatToken); err != nil { return false, err } if wechatToken.AccessToken == "" { return false, fmt.Errorf("invalid APISecret for CorpID: %s", n.conf.CorpID) } // Cache accessToken n.accessToken = wechatToken.AccessToken n.accessTokenAt = time.Now() } msg := &weChatMessage{ Text: weChatMessageContent{ Content: tmpl(n.conf.Message), }, ToUser: tmpl(n.conf.ToUser), ToParty: tmpl(n.conf.ToParty), Totag: tmpl(n.conf.ToTag), AgentID: tmpl(n.conf.AgentID), Type: "text", Safe: "0", } if err != nil { return false, fmt.Errorf("templating error: %s", err) } var buf bytes.Buffer if err := json.NewEncoder(&buf).Encode(msg); err != nil { return false, err } postMessageURL := n.conf.APIURL.Copy() postMessageURL.Path += "message/send" q := postMessageURL.Query() q.Set("access_token", n.accessToken) postMessageURL.RawQuery = q.Encode() req, err := http.NewRequest(http.MethodPost, postMessageURL.String(), &buf) if err != nil { return true, err } resp, err := c.Do(req.WithContext(ctx)) if err != nil { return true, err } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { return true, err } level.Debug(n.logger).Log("msg", "response: "+string(body), "incident", key) if resp.StatusCode != 200 { return true, fmt.Errorf("unexpected status code %v", resp.StatusCode) } else { var weResp weChatResponse if err := json.Unmarshal(body, &weResp); err != nil { return true, err } // https://work.weixin.qq.com/api/doc#10649 if weResp.Code == 0 { return false, nil } // AccessToken is expired if weResp.Code == 42001 { n.accessToken = "" return true, errors.New(weResp.Error) } return false, errors.New(weResp.Error) } } // OpsGenie implements a Notifier for OpsGenie notifications. type OpsGenie struct { conf *config.OpsGenieConfig tmpl *template.Template logger log.Logger } // NewOpsGenie returns a new OpsGenie notifier. func NewOpsGenie(c *config.OpsGenieConfig, t *template.Template, l log.Logger) *OpsGenie { return &OpsGenie{conf: c, tmpl: t, logger: l} } type opsGenieCreateMessage struct { Alias string `json:"alias"` Message string `json:"message"` Description string `json:"description,omitempty"` Details map[string]string `json:"details"` Source string `json:"source"` Teams []map[string]string `json:"teams,omitempty"` Tags []string `json:"tags,omitempty"` Note string `json:"note,omitempty"` Priority string `json:"priority,omitempty"` } type opsGenieCloseMessage struct { Source string `json:"source"` } // Notify implements the Notifier interface. func (n *OpsGenie) Notify(ctx context.Context, as ...*types.Alert) (bool, error) { req, retry, err := n.createRequest(ctx, as...) if err != nil { return retry, err } c, err := commoncfg.NewClientFromConfig(*n.conf.HTTPConfig, "opsgenie") if err != nil { return false, err } resp, err := ctxhttp.Do(ctx, c, req) if err != nil { return true, err } defer resp.Body.Close() return n.retry(resp.StatusCode) } // Like Split but filter out empty strings. func safeSplit(s string, sep string) []string { a := strings.Split(strings.TrimSpace(s), sep) b := a[:0] for _, x := range a { if x != "" { b = append(b, x) } } return b } // Create requests for a list of alerts. func (n *OpsGenie) createRequest(ctx context.Context, as ...*types.Alert) (*http.Request, bool, error) { key, ok := GroupKey(ctx) if !ok { return nil, false, fmt.Errorf("group key missing") } data := n.tmpl.Data(receiverName(ctx, n.logger), groupLabels(ctx, n.logger), as...) level.Debug(n.logger).Log("msg", "Notifying OpsGenie", "incident", key) var err error tmpl := tmplText(n.tmpl, data, &err) details := make(map[string]string, len(n.conf.Details)) for k, v := range n.conf.Details { details[k] = tmpl(v) } var ( msg interface{} apiURL = n.conf.APIURL.Copy() alias = hashKey(key) alerts = types.Alerts(as...) ) switch alerts.Status() { case model.AlertResolved: apiURL.Path += fmt.Sprintf("v2/alerts/%s/close", alias) q := apiURL.Query() q.Set("identifierType", "alias") apiURL.RawQuery = q.Encode() msg = &opsGenieCloseMessage{Source: tmpl(n.conf.Source)} default: message := tmpl(n.conf.Message) if len(message) > 130 { message = message[:127] + "..." level.Debug(n.logger).Log("msg", "Truncated message to %q due to OpsGenie message limit", "truncated_message", message, "incident", key) } apiURL.Path += "v2/alerts" var teams []map[string]string for _, t := range safeSplit(string(tmpl(n.conf.Teams)), ",") { teams = append(teams, map[string]string{"name": t}) } tags := safeSplit(string(tmpl(n.conf.Tags)), ",") msg = &opsGenieCreateMessage{ Alias: alias, Message: message, Description: tmpl(n.conf.Description), Details: details, Source: tmpl(n.conf.Source), Teams: teams, Tags: tags, Note: tmpl(n.conf.Note), Priority: tmpl(n.conf.Priority), } } if err != nil { return nil, false, fmt.Errorf("templating error: %s", err) } var buf bytes.Buffer if err := json.NewEncoder(&buf).Encode(msg); err != nil { return nil, false, err } req, err := http.NewRequest("POST", apiURL.String(), &buf) if err != nil { return nil, true, err } req.Header.Set("Content-Type", contentTypeJSON) req.Header.Set("Authorization", fmt.Sprintf("GenieKey %s", n.conf.APIKey)) return req, true, nil } func (n *OpsGenie) retry(statusCode int) (bool, error) { // https://docs.opsgenie.com/docs/response#section-response-codes // Response codes 429 (rate limiting) and 5xx are potentially recoverable if statusCode/100 == 5 || statusCode == 429 { return true, fmt.Errorf("unexpected status code %v", statusCode) } else if statusCode/100 != 2 { return false, fmt.Errorf("unexpected status code %v", statusCode) } return false, nil } // VictorOps implements a Notifier for VictorOps notifications. type VictorOps struct { conf *config.VictorOpsConfig tmpl *template.Template logger log.Logger } // NewVictorOps returns a new VictorOps notifier. func NewVictorOps(c *config.VictorOpsConfig, t *template.Template, l log.Logger) *VictorOps { return &VictorOps{ conf: c, tmpl: t, logger: l, } } const ( victorOpsEventTrigger = "CRITICAL" victorOpsEventResolve = "RECOVERY" ) type victorOpsMessage struct { MessageType string `json:"message_type"` EntityID string `json:"entity_id"` EntityDisplayName string `json:"entity_display_name"` StateMessage string `json:"state_message"` MonitoringTool string `json:"monitoring_tool"` } // Notify implements the Notifier interface. func (n *VictorOps) Notify(ctx context.Context, as ...*types.Alert) (bool, error) { victorOpsAllowedEvents := map[string]bool{ "INFO": true, "WARNING": true, "CRITICAL": true, } key, ok := GroupKey(ctx) if !ok { return false, fmt.Errorf("group key missing") } var err error var ( alerts = types.Alerts(as...) data = n.tmpl.Data(receiverName(ctx, n.logger), groupLabels(ctx, n.logger), as...) tmpl = tmplText(n.tmpl, data, &err) apiURL = n.conf.APIURL.Copy() messageType = tmpl(n.conf.MessageType) stateMessage = tmpl(n.conf.StateMessage) ) apiURL.Path += fmt.Sprintf("%s/%s", n.conf.APIKey, tmpl(n.conf.RoutingKey)) if alerts.Status() == model.AlertFiring && !victorOpsAllowedEvents[messageType] { messageType = victorOpsEventTrigger } if alerts.Status() == model.AlertResolved { messageType = victorOpsEventResolve } if len(stateMessage) > 20480 { stateMessage = stateMessage[0:20475] + "\n..." level.Debug(n.logger).Log("msg", "Truncated stateMessage due to VictorOps stateMessage limit", "truncated_state_message", stateMessage, "incident", key) } msg := &victorOpsMessage{ MessageType: messageType, EntityID: hashKey(key), EntityDisplayName: tmpl(n.conf.EntityDisplayName), StateMessage: stateMessage, MonitoringTool: tmpl(n.conf.MonitoringTool), } if err != nil { return false, fmt.Errorf("templating error: %s", err) } var buf bytes.Buffer if err := json.NewEncoder(&buf).Encode(msg); err != nil { return false, err } c, err := commoncfg.NewClientFromConfig(*n.conf.HTTPConfig, "victorops") if err != nil { return false, err } resp, err := ctxhttp.Post(ctx, c, apiURL.String(), contentTypeJSON, &buf) if err != nil { return true, err } defer resp.Body.Close() return n.retry(resp.StatusCode) } func (n *VictorOps) retry(statusCode int) (bool, error) { // Missing documentation therefore assuming only 5xx response codes are // recoverable. if statusCode/100 == 5 { return true, fmt.Errorf("unexpected status code %v", statusCode) } else if statusCode/100 != 2 { return false, fmt.Errorf("unexpected status code %v", statusCode) } return false, nil } // Pushover implements a Notifier for Pushover notifications. type Pushover struct { conf *config.PushoverConfig tmpl *template.Template logger log.Logger } // NewPushover returns a new Pushover notifier. func NewPushover(c *config.PushoverConfig, t *template.Template, l log.Logger) *Pushover { return &Pushover{conf: c, tmpl: t, logger: l} } // Notify implements the Notifier interface. func (n *Pushover) Notify(ctx context.Context, as ...*types.Alert) (bool, error) { key, ok := GroupKey(ctx) if !ok { return false, fmt.Errorf("group key missing") } data := n.tmpl.Data(receiverName(ctx, n.logger), groupLabels(ctx, n.logger), as...) level.Debug(n.logger).Log("msg", "Notifying Pushover", "incident", key) var err error tmpl := tmplText(n.tmpl, data, &err) parameters := url.Values{} parameters.Add("token", tmpl(string(n.conf.Token))) parameters.Add("user", tmpl(string(n.conf.UserKey))) title := tmpl(n.conf.Title) if len(title) > 250 { title = title[:247] + "..." level.Debug(n.logger).Log("msg", "Truncated title due to Pushover title limit", "truncated_title", title, "incident", key) } parameters.Add("title", title) message := tmpl(n.conf.Message) if len(message) > 1024 { message = message[:1021] + "..." level.Debug(n.logger).Log("msg", "Truncated message due to Pushover message limit", "truncated_message", message, "incident", key) } message = strings.TrimSpace(message) if message == "" { // Pushover rejects empty messages. message = "(no details)" } parameters.Add("message", message) supplementaryURL := tmpl(n.conf.URL) if len(supplementaryURL) > 512 { supplementaryURL = supplementaryURL[:509] + "..." level.Debug(n.logger).Log("msg", "Truncated URL due to Pushover url limit", "truncated_url", supplementaryURL, "incident", key) } parameters.Add("url", supplementaryURL) parameters.Add("priority", tmpl(n.conf.Priority)) parameters.Add("retry", fmt.Sprintf("%d", int64(time.Duration(n.conf.Retry).Seconds()))) parameters.Add("expire", fmt.Sprintf("%d", int64(time.Duration(n.conf.Expire).Seconds()))) if err != nil { return false, err } apiURL := "https://api.pushover.net/1/messages.json" u, err := url.Parse(apiURL) if err != nil { return false, err } u.RawQuery = parameters.Encode() level.Debug(n.logger).Log("msg", "Sending Pushover message", "incident", key, "url", u.String()) c, err := commoncfg.NewClientFromConfig(*n.conf.HTTPConfig, "pushover") if err != nil { return false, err } resp, err := ctxhttp.Post(ctx, c, u.String(), "text/plain", nil) if err != nil { return true, err } defer resp.Body.Close() return n.retry(resp.StatusCode) } func (n *Pushover) retry(statusCode int) (bool, error) { // Only documented behaviour is that 2xx response codes are successful and // 4xx are unsuccessful, therefore assuming only 5xx are recoverable. // https://pushover.net/api#response if statusCode/100 == 5 { return true, fmt.Errorf("unexpected status code %v", statusCode) } else if statusCode/100 != 2 { return false, fmt.Errorf("unexpected status code %v", statusCode) } return false, nil } // tmplText is using monadic error handling in order to make string templating // less verbose. Use with care as the final error checking is easily missed. func tmplText(tmpl *template.Template, data *template.Data, err *error) func(string) string { return func(name string) (s string) { if *err != nil { return } s, *err = tmpl.ExecuteTextString(name, data) return s } } // tmplHTML is using monadic error handling in order to make string templating // less verbose. Use with care as the final error checking is easily missed. func tmplHTML(tmpl *template.Template, data *template.Data, err *error) func(string) string { return func(name string) (s string) { if *err != nil { return } s, *err = tmpl.ExecuteHTMLString(name, data) return s } } type loginAuth struct { username, password string } func LoginAuth(username, password string) smtp.Auth { return &loginAuth{username, password} } func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) { return "LOGIN", []byte{}, nil } // Used for AUTH LOGIN. (Maybe password should be encrypted) func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) { if more { switch strings.ToLower(string(fromServer)) { case "username:": return []byte(a.username), nil case "password:": return []byte(a.password), nil default: return nil, errors.New("unexpected server challenge") } } return nil, nil } // hashKey returns the sha256 for a group key as integrations may have // maximum length requirements on deduplication keys. func hashKey(s string) string { h := sha256.New() h.Write([]byte(s)) return fmt.Sprintf("%x", h.Sum(nil)) } prometheus-alertmanager-0.15.3+ds/notify/impl_test.go000066400000000000000000000224301341674552200227060ustar00rootroot00000000000000// Copyright 2018 Prometheus Team // 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 notify import ( "fmt" "io/ioutil" "net/http" "net/url" "testing" "time" "github.com/go-kit/kit/log" "github.com/stretchr/testify/require" "golang.org/x/net/context" "github.com/prometheus/alertmanager/config" "github.com/prometheus/alertmanager/template" "github.com/prometheus/alertmanager/types" "github.com/prometheus/common/model" ) func TestWebhookRetry(t *testing.T) { u, err := url.Parse("http://example.com") if err != nil { t.Fatalf("failed to parse URL: %v", err) } notifier := &Webhook{conf: &config.WebhookConfig{URL: &config.URL{u}}} for statusCode, expected := range retryTests(defaultRetryCodes()) { actual, _ := notifier.retry(statusCode) require.Equal(t, expected, actual, fmt.Sprintf("error on status %d", statusCode)) } } func TestPagerDutyRetryV1(t *testing.T) { notifier := new(PagerDuty) retryCodes := append(defaultRetryCodes(), http.StatusForbidden) for statusCode, expected := range retryTests(retryCodes) { resp := &http.Response{ StatusCode: statusCode, } actual, _ := notifier.retryV1(resp) require.Equal(t, expected, actual, fmt.Sprintf("retryv1 - error on status %d", statusCode)) } } func TestPagerDutyRetryV2(t *testing.T) { notifier := new(PagerDuty) retryCodes := append(defaultRetryCodes(), http.StatusTooManyRequests) for statusCode, expected := range retryTests(retryCodes) { actual, _ := notifier.retryV2(statusCode) require.Equal(t, expected, actual, fmt.Sprintf("retryv2 - error on status %d", statusCode)) } } func TestSlackRetry(t *testing.T) { notifier := new(Slack) for statusCode, expected := range retryTests(defaultRetryCodes()) { actual, _ := notifier.retry(statusCode) require.Equal(t, expected, actual, fmt.Sprintf("error on status %d", statusCode)) } } func TestHipchatRetry(t *testing.T) { notifier := new(Hipchat) retryCodes := append(defaultRetryCodes(), http.StatusTooManyRequests) for statusCode, expected := range retryTests(retryCodes) { actual, _ := notifier.retry(statusCode) require.Equal(t, expected, actual, fmt.Sprintf("error on status %d", statusCode)) } } func TestOpsGenieRetry(t *testing.T) { notifier := new(OpsGenie) retryCodes := append(defaultRetryCodes(), http.StatusTooManyRequests) for statusCode, expected := range retryTests(retryCodes) { actual, _ := notifier.retry(statusCode) require.Equal(t, expected, actual, fmt.Sprintf("error on status %d", statusCode)) } } func TestVictorOpsRetry(t *testing.T) { notifier := new(VictorOps) for statusCode, expected := range retryTests(defaultRetryCodes()) { actual, _ := notifier.retry(statusCode) require.Equal(t, expected, actual, fmt.Sprintf("error on status %d", statusCode)) } } func TestPushoverRetry(t *testing.T) { notifier := new(Pushover) for statusCode, expected := range retryTests(defaultRetryCodes()) { actual, _ := notifier.retry(statusCode) require.Equal(t, expected, actual, fmt.Sprintf("error on status %d", statusCode)) } } func retryTests(retryCodes []int) map[int]bool { tests := map[int]bool{ // 1xx http.StatusContinue: false, http.StatusSwitchingProtocols: false, http.StatusProcessing: false, // 2xx http.StatusOK: false, http.StatusCreated: false, http.StatusAccepted: false, http.StatusNonAuthoritativeInfo: false, http.StatusNoContent: false, http.StatusResetContent: false, http.StatusPartialContent: false, http.StatusMultiStatus: false, http.StatusAlreadyReported: false, http.StatusIMUsed: false, // 3xx http.StatusMultipleChoices: false, http.StatusMovedPermanently: false, http.StatusFound: false, http.StatusSeeOther: false, http.StatusNotModified: false, http.StatusUseProxy: false, http.StatusTemporaryRedirect: false, http.StatusPermanentRedirect: false, // 4xx http.StatusBadRequest: false, http.StatusUnauthorized: false, http.StatusPaymentRequired: false, http.StatusForbidden: false, http.StatusNotFound: false, http.StatusMethodNotAllowed: false, http.StatusNotAcceptable: false, http.StatusProxyAuthRequired: false, http.StatusRequestTimeout: false, http.StatusConflict: false, http.StatusGone: false, http.StatusLengthRequired: false, http.StatusPreconditionFailed: false, http.StatusRequestEntityTooLarge: false, http.StatusRequestURITooLong: false, http.StatusUnsupportedMediaType: false, http.StatusRequestedRangeNotSatisfiable: false, http.StatusExpectationFailed: false, http.StatusTeapot: false, http.StatusUnprocessableEntity: false, http.StatusLocked: false, http.StatusFailedDependency: false, http.StatusUpgradeRequired: false, http.StatusPreconditionRequired: false, http.StatusTooManyRequests: false, http.StatusRequestHeaderFieldsTooLarge: false, http.StatusUnavailableForLegalReasons: false, // 5xx http.StatusInternalServerError: false, http.StatusNotImplemented: false, http.StatusBadGateway: false, http.StatusServiceUnavailable: false, http.StatusGatewayTimeout: false, http.StatusHTTPVersionNotSupported: false, http.StatusVariantAlsoNegotiates: false, http.StatusInsufficientStorage: false, http.StatusLoopDetected: false, http.StatusNotExtended: false, http.StatusNetworkAuthenticationRequired: false, } for _, statusCode := range retryCodes { tests[statusCode] = true } return tests } func defaultRetryCodes() []int { return []int{ http.StatusInternalServerError, http.StatusNotImplemented, http.StatusBadGateway, http.StatusServiceUnavailable, http.StatusGatewayTimeout, http.StatusHTTPVersionNotSupported, http.StatusVariantAlsoNegotiates, http.StatusInsufficientStorage, http.StatusLoopDetected, http.StatusNotExtended, http.StatusNetworkAuthenticationRequired, } } func createTmpl(t *testing.T) *template.Template { tmpl, err := template.FromGlobs() require.NoError(t, err) tmpl.ExternalURL, _ = url.Parse("http://am") return tmpl } func readBody(t *testing.T, r *http.Request) string { body, err := ioutil.ReadAll(r.Body) require.NoError(t, err) return string(body) } func TestOpsGenie(t *testing.T) { u, err := url.Parse("https://opsgenie/api") if err != nil { t.Fatalf("failed to parse URL: %v", err) } logger := log.NewNopLogger() tmpl := createTmpl(t) conf := &config.OpsGenieConfig{ NotifierConfig: config.NotifierConfig{ VSendResolved: true, }, Message: `{{ .CommonLabels.Message }}`, Description: `{{ .CommonLabels.Description }}`, Source: `{{ .CommonLabels.Source }}`, Teams: `{{ .CommonLabels.Teams }}`, Tags: `{{ .CommonLabels.Tags }}`, Note: `{{ .CommonLabels.Note }}`, Priority: `{{ .CommonLabels.Priority }}`, APIKey: `s3cr3t`, APIURL: &config.URL{u}, } notifier := NewOpsGenie(conf, tmpl, logger) ctx := context.Background() ctx = WithGroupKey(ctx, "1") expectedURL, _ := url.Parse("https://opsgenie/apiv2/alerts") // Empty alert. alert1 := &types.Alert{ Alert: model.Alert{ StartsAt: time.Now(), EndsAt: time.Now().Add(time.Hour), }, } expectedBody := `{"alias":"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b","message":"","details":{},"source":""} ` req, retry, err := notifier.createRequest(ctx, alert1) require.NoError(t, err) require.Equal(t, true, retry) require.Equal(t, expectedURL, req.URL) require.Equal(t, "GenieKey s3cr3t", req.Header.Get("Authorization")) require.Equal(t, expectedBody, readBody(t, req)) // Fully defined alert. alert2 := &types.Alert{ Alert: model.Alert{ Labels: model.LabelSet{ "Message": "message", "Description": "description", "Source": "http://prometheus", "Teams": "TeamA,TeamB,", "Tags": "tag1,tag2", "Note": "this is a note", "Priotity": "P1", }, StartsAt: time.Now(), EndsAt: time.Now().Add(time.Hour), }, } expectedBody = `{"alias":"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b","message":"message","description":"description","details":{},"source":"http://prometheus","teams":[{"name":"TeamA"},{"name":"TeamB"}],"tags":["tag1","tag2"],"note":"this is a note"} ` req, retry, err = notifier.createRequest(ctx, alert2) require.NoError(t, err) require.Equal(t, true, retry) require.Equal(t, expectedBody, readBody(t, req)) } prometheus-alertmanager-0.15.3+ds/notify/notify.go000066400000000000000000000507441341674552200222270ustar00rootroot00000000000000// Copyright 2015 Prometheus Team // 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 notify import ( "fmt" "sort" "sync" "time" "github.com/cenkalti/backoff" "github.com/cespare/xxhash" "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/common/model" "golang.org/x/net/context" "github.com/prometheus/alertmanager/cluster" "github.com/prometheus/alertmanager/config" "github.com/prometheus/alertmanager/nflog" "github.com/prometheus/alertmanager/nflog/nflogpb" "github.com/prometheus/alertmanager/silence" "github.com/prometheus/alertmanager/template" "github.com/prometheus/alertmanager/types" ) var ( numNotifications = prometheus.NewCounterVec(prometheus.CounterOpts{ Namespace: "alertmanager", Name: "notifications_total", Help: "The total number of attempted notifications.", }, []string{"integration"}) numFailedNotifications = prometheus.NewCounterVec(prometheus.CounterOpts{ Namespace: "alertmanager", Name: "notifications_failed_total", Help: "The total number of failed notifications.", }, []string{"integration"}) notificationLatencySeconds = prometheus.NewHistogramVec(prometheus.HistogramOpts{ Namespace: "alertmanager", Name: "notification_latency_seconds", Help: "The latency of notifications in seconds.", Buckets: []float64{1, 5, 10, 15, 20}, }, []string{"integration"}) ) func init() { numNotifications.WithLabelValues("email") numNotifications.WithLabelValues("hipchat") numNotifications.WithLabelValues("pagerduty") numNotifications.WithLabelValues("wechat") numNotifications.WithLabelValues("pushover") numNotifications.WithLabelValues("slack") numNotifications.WithLabelValues("opsgenie") numNotifications.WithLabelValues("webhook") numNotifications.WithLabelValues("victorops") numFailedNotifications.WithLabelValues("email") numFailedNotifications.WithLabelValues("hipchat") numFailedNotifications.WithLabelValues("pagerduty") numFailedNotifications.WithLabelValues("wechat") numFailedNotifications.WithLabelValues("pushover") numFailedNotifications.WithLabelValues("slack") numFailedNotifications.WithLabelValues("opsgenie") numFailedNotifications.WithLabelValues("webhook") numFailedNotifications.WithLabelValues("victorops") notificationLatencySeconds.WithLabelValues("email") notificationLatencySeconds.WithLabelValues("hipchat") notificationLatencySeconds.WithLabelValues("pagerduty") notificationLatencySeconds.WithLabelValues("wechat") notificationLatencySeconds.WithLabelValues("pushover") notificationLatencySeconds.WithLabelValues("slack") notificationLatencySeconds.WithLabelValues("opsgenie") notificationLatencySeconds.WithLabelValues("webhook") notificationLatencySeconds.WithLabelValues("victorops") prometheus.MustRegister(numNotifications) prometheus.MustRegister(numFailedNotifications) prometheus.MustRegister(notificationLatencySeconds) } type notifierConfig interface { SendResolved() bool } // MinTimeout is the minimum timeout that is set for the context of a call // to a notification pipeline. const MinTimeout = 10 * time.Second // notifyKey defines a custom type with which a context is populated to // avoid accidental collisions. type notifyKey int const ( keyReceiverName notifyKey = iota keyRepeatInterval keyGroupLabels keyGroupKey keyFiringAlerts keyResolvedAlerts keyNow ) // WithReceiverName populates a context with a receiver name. func WithReceiverName(ctx context.Context, rcv string) context.Context { return context.WithValue(ctx, keyReceiverName, rcv) } // WithGroupKey populates a context with a group key. func WithGroupKey(ctx context.Context, s string) context.Context { return context.WithValue(ctx, keyGroupKey, s) } // WithFiringAlerts populates a context with a slice of firing alerts. func WithFiringAlerts(ctx context.Context, alerts []uint64) context.Context { return context.WithValue(ctx, keyFiringAlerts, alerts) } // WithResolvedAlerts populates a context with a slice of resolved alerts. func WithResolvedAlerts(ctx context.Context, alerts []uint64) context.Context { return context.WithValue(ctx, keyResolvedAlerts, alerts) } // WithGroupLabels populates a context with grouping labels. func WithGroupLabels(ctx context.Context, lset model.LabelSet) context.Context { return context.WithValue(ctx, keyGroupLabels, lset) } // WithNow populates a context with a now timestamp. func WithNow(ctx context.Context, t time.Time) context.Context { return context.WithValue(ctx, keyNow, t) } // WithRepeatInterval populates a context with a repeat interval. func WithRepeatInterval(ctx context.Context, t time.Duration) context.Context { return context.WithValue(ctx, keyRepeatInterval, t) } // RepeatInterval extracts a repeat interval from the context. Iff none exists, the // second argument is false. func RepeatInterval(ctx context.Context) (time.Duration, bool) { v, ok := ctx.Value(keyRepeatInterval).(time.Duration) return v, ok } // ReceiverName extracts a receiver name from the context. Iff none exists, the // second argument is false. func ReceiverName(ctx context.Context) (string, bool) { v, ok := ctx.Value(keyReceiverName).(string) return v, ok } func receiverName(ctx context.Context, l log.Logger) string { recv, ok := ReceiverName(ctx) if !ok { level.Error(l).Log("msg", "Missing receiver") } return recv } // GroupKey extracts a group key from the context. Iff none exists, the // second argument is false. func GroupKey(ctx context.Context) (string, bool) { v, ok := ctx.Value(keyGroupKey).(string) return v, ok } func groupLabels(ctx context.Context, l log.Logger) model.LabelSet { groupLabels, ok := GroupLabels(ctx) if !ok { level.Error(l).Log("msg", "Missing group labels") } return groupLabels } // GroupLabels extracts grouping label set from the context. Iff none exists, the // second argument is false. func GroupLabels(ctx context.Context) (model.LabelSet, bool) { v, ok := ctx.Value(keyGroupLabels).(model.LabelSet) return v, ok } // Now extracts a now timestamp from the context. Iff none exists, the // second argument is false. func Now(ctx context.Context) (time.Time, bool) { v, ok := ctx.Value(keyNow).(time.Time) return v, ok } // FiringAlerts extracts a slice of firing alerts from the context. // Iff none exists, the second argument is false. func FiringAlerts(ctx context.Context) ([]uint64, bool) { v, ok := ctx.Value(keyFiringAlerts).([]uint64) return v, ok } // ResolvedAlerts extracts a slice of firing alerts from the context. // Iff none exists, the second argument is false. func ResolvedAlerts(ctx context.Context) ([]uint64, bool) { v, ok := ctx.Value(keyResolvedAlerts).([]uint64) return v, ok } // A Stage processes alerts under the constraints of the given context. type Stage interface { Exec(ctx context.Context, l log.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error) } // StageFunc wraps a function to represent a Stage. type StageFunc func(ctx context.Context, l log.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error) // Exec implements Stage interface. func (f StageFunc) Exec(ctx context.Context, l log.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error) { return f(ctx, l, alerts...) } type NotificationLog interface { Log(r *nflogpb.Receiver, gkey string, firingAlerts, resolvedAlerts []uint64) error Query(params ...nflog.QueryParam) ([]*nflogpb.Entry, error) } // BuildPipeline builds a map of receivers to Stages. func BuildPipeline( confs []*config.Receiver, tmpl *template.Template, wait func() time.Duration, muter types.Muter, silences *silence.Silences, notificationLog NotificationLog, marker types.Marker, peer *cluster.Peer, logger log.Logger, ) RoutingStage { rs := RoutingStage{} ms := NewGossipSettleStage(peer) is := NewInhibitStage(muter) ss := NewSilenceStage(silences, marker) for _, rc := range confs { rs[rc.Name] = MultiStage{ms, is, ss, createStage(rc, tmpl, wait, notificationLog, logger)} } return rs } // createStage creates a pipeline of stages for a receiver. func createStage(rc *config.Receiver, tmpl *template.Template, wait func() time.Duration, notificationLog NotificationLog, logger log.Logger) Stage { var fs FanoutStage for _, i := range BuildReceiverIntegrations(rc, tmpl, logger) { recv := &nflogpb.Receiver{ GroupName: rc.Name, Integration: i.name, Idx: uint32(i.idx), } var s MultiStage s = append(s, NewWaitStage(wait)) s = append(s, NewDedupStage(i, notificationLog, recv)) s = append(s, NewRetryStage(i, rc.Name)) s = append(s, NewSetNotifiesStage(notificationLog, recv)) fs = append(fs, s) } return fs } // RoutingStage executes the inner stages based on the receiver specified in // the context. type RoutingStage map[string]Stage // Exec implements the Stage interface. func (rs RoutingStage) Exec(ctx context.Context, l log.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error) { receiver, ok := ReceiverName(ctx) if !ok { return ctx, nil, fmt.Errorf("receiver missing") } s, ok := rs[receiver] if !ok { return ctx, nil, fmt.Errorf("stage for receiver missing") } return s.Exec(ctx, l, alerts...) } // A MultiStage executes a series of stages sequencially. type MultiStage []Stage // Exec implements the Stage interface. func (ms MultiStage) Exec(ctx context.Context, l log.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error) { var err error for _, s := range ms { if len(alerts) == 0 { return ctx, nil, nil } ctx, alerts, err = s.Exec(ctx, l, alerts...) if err != nil { return ctx, nil, err } } return ctx, alerts, nil } // FanoutStage executes its stages concurrently type FanoutStage []Stage // Exec attempts to execute all stages concurrently and discards the results. // It returns its input alerts and a types.MultiError if one or more stages fail. func (fs FanoutStage) Exec(ctx context.Context, l log.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error) { var ( wg sync.WaitGroup me types.MultiError ) wg.Add(len(fs)) for _, s := range fs { go func(s Stage) { if _, _, err := s.Exec(ctx, l, alerts...); err != nil { me.Add(err) level.Error(l).Log("msg", "Error on notify", "err", err) } wg.Done() }(s) } wg.Wait() if me.Len() > 0 { return ctx, alerts, &me } return ctx, alerts, nil } // GossipSettleStage waits until the Gossip has settled to forward alerts. type GossipSettleStage struct { peer *cluster.Peer } // NewGossipSettleStage returns a new GossipSettleStage. func NewGossipSettleStage(p *cluster.Peer) *GossipSettleStage { return &GossipSettleStage{peer: p} } func (n *GossipSettleStage) Exec(ctx context.Context, l log.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error) { if n.peer != nil { n.peer.WaitReady() } return ctx, alerts, nil } // InhibitStage filters alerts through an inhibition muter. type InhibitStage struct { muter types.Muter } // NewInhibitStage return a new InhibitStage. func NewInhibitStage(m types.Muter) *InhibitStage { return &InhibitStage{muter: m} } // Exec implements the Stage interface. func (n *InhibitStage) Exec(ctx context.Context, l log.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error) { var filtered []*types.Alert for _, a := range alerts { // TODO(fabxc): increment total alerts counter. // Do not send the alert if the silencer mutes it. if !n.muter.Mutes(a.Labels) { // TODO(fabxc): increment muted alerts counter. filtered = append(filtered, a) } } return ctx, filtered, nil } // SilenceStage filters alerts through a silence muter. type SilenceStage struct { silences *silence.Silences marker types.Marker } // NewSilenceStage returns a new SilenceStage. func NewSilenceStage(s *silence.Silences, mk types.Marker) *SilenceStage { return &SilenceStage{ silences: s, marker: mk, } } // Exec implements the Stage interface. func (n *SilenceStage) Exec(ctx context.Context, l log.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error) { var filtered []*types.Alert for _, a := range alerts { // TODO(fabxc): increment total alerts counter. // Do not send the alert if the silencer mutes it. sils, err := n.silences.Query( silence.QState(types.SilenceStateActive), silence.QMatches(a.Labels), ) if err != nil { level.Error(l).Log("msg", "Querying silences failed", "err", err) } if len(sils) == 0 { // TODO(fabxc): increment muted alerts counter. filtered = append(filtered, a) n.marker.SetSilenced(a.Labels.Fingerprint()) } else { ids := make([]string, len(sils)) for i, s := range sils { ids[i] = s.Id } n.marker.SetSilenced(a.Labels.Fingerprint(), ids...) } } return ctx, filtered, nil } // WaitStage waits for a certain amount of time before continuing or until the // context is done. type WaitStage struct { wait func() time.Duration } // NewWaitStage returns a new WaitStage. func NewWaitStage(wait func() time.Duration) *WaitStage { return &WaitStage{ wait: wait, } } // Exec implements the Stage interface. func (ws *WaitStage) Exec(ctx context.Context, l log.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error) { select { case <-time.After(ws.wait()): case <-ctx.Done(): return ctx, nil, ctx.Err() } return ctx, alerts, nil } // DedupStage filters alerts. // Filtering happens based on a notification log. type DedupStage struct { nflog NotificationLog recv *nflogpb.Receiver conf notifierConfig now func() time.Time hash func(*types.Alert) uint64 } // NewDedupStage wraps a DedupStage that runs against the given notification log. func NewDedupStage(i Integration, l NotificationLog, recv *nflogpb.Receiver) *DedupStage { return &DedupStage{ nflog: l, recv: recv, conf: i.conf, now: utcNow, hash: hashAlert, } } func utcNow() time.Time { return time.Now().UTC() } var hashBuffers = sync.Pool{} func getHashBuffer() []byte { b := hashBuffers.Get() if b == nil { return make([]byte, 0, 1024) } return b.([]byte) } func putHashBuffer(b []byte) { b = b[:0] hashBuffers.Put(b) } func hashAlert(a *types.Alert) uint64 { const sep = '\xff' b := getHashBuffer() defer putHashBuffer(b) names := make(model.LabelNames, 0, len(a.Labels)) for ln := range a.Labels { names = append(names, ln) } sort.Sort(names) for _, ln := range names { b = append(b, string(ln)...) b = append(b, sep) b = append(b, string(a.Labels[ln])...) b = append(b, sep) } hash := xxhash.Sum64(b) return hash } func (n *DedupStage) needsUpdate(entry *nflogpb.Entry, firing, resolved map[uint64]struct{}, repeat time.Duration) bool { // If we haven't notified about the alert group before, notify right away // unless we only have resolved alerts. if entry == nil { return len(firing) > 0 } if !entry.IsFiringSubset(firing) { return true } // Notify about all alerts being resolved. // This is done irrespective of the send_resolved flag to make sure that // the firing alerts are cleared from the notification log. if len(firing) == 0 { // If the current alert group and last notification contain no firing // alert, it means that some alerts have been fired and resolved during the // last interval. In this case, there is no need to notify the receiver // since it doesn't know about them. return len(entry.FiringAlerts) > 0 } if n.conf.SendResolved() && !entry.IsResolvedSubset(resolved) { return true } // Nothing changed, only notify if the repeat interval has passed. return entry.Timestamp.Before(n.now().Add(-repeat)) } // Exec implements the Stage interface. func (n *DedupStage) Exec(ctx context.Context, l log.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error) { gkey, ok := GroupKey(ctx) if !ok { return ctx, nil, fmt.Errorf("group key missing") } repeatInterval, ok := RepeatInterval(ctx) if !ok { return ctx, nil, fmt.Errorf("repeat interval missing") } firingSet := map[uint64]struct{}{} resolvedSet := map[uint64]struct{}{} firing := []uint64{} resolved := []uint64{} var hash uint64 for _, a := range alerts { hash = n.hash(a) if a.Resolved() { resolved = append(resolved, hash) resolvedSet[hash] = struct{}{} } else { firing = append(firing, hash) firingSet[hash] = struct{}{} } } ctx = WithFiringAlerts(ctx, firing) ctx = WithResolvedAlerts(ctx, resolved) entries, err := n.nflog.Query(nflog.QGroupKey(gkey), nflog.QReceiver(n.recv)) if err != nil && err != nflog.ErrNotFound { return ctx, nil, err } var entry *nflogpb.Entry switch len(entries) { case 0: case 1: entry = entries[0] case 2: return ctx, nil, fmt.Errorf("unexpected entry result size %d", len(entries)) } if n.needsUpdate(entry, firingSet, resolvedSet, repeatInterval) { return ctx, alerts, nil } return ctx, nil, nil } // RetryStage notifies via passed integration with exponential backoff until it // succeeds. It aborts if the context is canceled or timed out. type RetryStage struct { integration Integration groupName string } // NewRetryStage returns a new instance of a RetryStage. func NewRetryStage(i Integration, groupName string) *RetryStage { return &RetryStage{ integration: i, groupName: groupName, } } // Exec implements the Stage interface. func (r RetryStage) Exec(ctx context.Context, l log.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error) { var sent []*types.Alert // If we shouldn't send notifications for resolved alerts, but there are only // resolved alerts, report them all as successfully notified (we still want the // notification log to log them for the next run of DedupStage). if !r.integration.conf.SendResolved() { firing, ok := FiringAlerts(ctx) if !ok { return ctx, nil, fmt.Errorf("firing alerts missing") } if len(firing) == 0 { return ctx, alerts, nil } for _, a := range alerts { if a.Status() != model.AlertResolved { sent = append(sent, a) } } } else { sent = alerts } var ( i = 0 b = backoff.NewExponentialBackOff() tick = backoff.NewTicker(b) iErr error ) defer tick.Stop() for { i++ // Always check the context first to not notify again. select { case <-ctx.Done(): if iErr != nil { return ctx, nil, iErr } return ctx, nil, ctx.Err() default: } select { case <-tick.C: now := time.Now() retry, err := r.integration.Notify(ctx, sent...) notificationLatencySeconds.WithLabelValues(r.integration.name).Observe(time.Since(now).Seconds()) if err != nil { numFailedNotifications.WithLabelValues(r.integration.name).Inc() level.Debug(l).Log("msg", "Notify attempt failed", "attempt", i, "integration", r.integration.name, "receiver", r.groupName, "err", err) if !retry { return ctx, alerts, fmt.Errorf("cancelling notify retry for %q due to unrecoverable error: %s", r.integration.name, err) } // Save this error to be able to return the last seen error by an // integration upon context timeout. iErr = err } else { numNotifications.WithLabelValues(r.integration.name).Inc() return ctx, alerts, nil } case <-ctx.Done(): if iErr != nil { return ctx, nil, iErr } return ctx, nil, ctx.Err() } } } // SetNotifiesStage sets the notification information about passed alerts. The // passed alerts should have already been sent to the receivers. type SetNotifiesStage struct { nflog NotificationLog recv *nflogpb.Receiver } // NewSetNotifiesStage returns a new instance of a SetNotifiesStage. func NewSetNotifiesStage(l NotificationLog, recv *nflogpb.Receiver) *SetNotifiesStage { return &SetNotifiesStage{ nflog: l, recv: recv, } } // Exec implements the Stage interface. func (n SetNotifiesStage) Exec(ctx context.Context, l log.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error) { gkey, ok := GroupKey(ctx) if !ok { return ctx, nil, fmt.Errorf("group key missing") } firing, ok := FiringAlerts(ctx) if !ok { return ctx, nil, fmt.Errorf("firing alerts missing") } resolved, ok := ResolvedAlerts(ctx) if !ok { return ctx, nil, fmt.Errorf("resolved alerts missing") } return ctx, alerts, n.nflog.Log(n.recv, gkey, firing, resolved) } prometheus-alertmanager-0.15.3+ds/notify/notify_test.go000066400000000000000000000427361341674552200232700ustar00rootroot00000000000000// Copyright 2015 Prometheus Team // 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 notify import ( "errors" "fmt" "io" "reflect" "testing" "time" "github.com/go-kit/kit/log" "github.com/prometheus/common/model" "github.com/stretchr/testify/require" "golang.org/x/net/context" "github.com/prometheus/alertmanager/nflog" "github.com/prometheus/alertmanager/nflog/nflogpb" "github.com/prometheus/alertmanager/silence" "github.com/prometheus/alertmanager/silence/silencepb" "github.com/prometheus/alertmanager/types" ) type notifierConfigFunc func() bool func (f notifierConfigFunc) SendResolved() bool { return f() } type notifierFunc func(ctx context.Context, alerts ...*types.Alert) (bool, error) func (f notifierFunc) Notify(ctx context.Context, alerts ...*types.Alert) (bool, error) { return f(ctx, alerts...) } type failStage struct{} func (s failStage) Exec(ctx context.Context, l log.Logger, as ...*types.Alert) (context.Context, []*types.Alert, error) { return ctx, nil, fmt.Errorf("some error") } type testNflog struct { qres []*nflogpb.Entry qerr error logFunc func(r *nflogpb.Receiver, gkey string, firingAlerts, resolvedAlerts []uint64) error } func (l *testNflog) Query(p ...nflog.QueryParam) ([]*nflogpb.Entry, error) { return l.qres, l.qerr } func (l *testNflog) Log(r *nflogpb.Receiver, gkey string, firingAlerts, resolvedAlerts []uint64) error { return l.logFunc(r, gkey, firingAlerts, resolvedAlerts) } func (l *testNflog) GC() (int, error) { return 0, nil } func (l *testNflog) Snapshot(w io.Writer) (int, error) { return 0, nil } func alertHashSet(hashes ...uint64) map[uint64]struct{} { res := map[uint64]struct{}{} for _, h := range hashes { res[h] = struct{}{} } return res } func TestDedupStageNeedsUpdate(t *testing.T) { now := utcNow() cases := []struct { entry *nflogpb.Entry firingAlerts map[uint64]struct{} resolvedAlerts map[uint64]struct{} repeat time.Duration resolve bool res bool }{ { // No matching nflog entry should update. entry: nil, firingAlerts: alertHashSet(2, 3, 4), res: true, }, { // No matching nflog entry shouldn't update if no alert fires. entry: nil, resolvedAlerts: alertHashSet(2, 3, 4), res: false, }, { // Different sets of firing alerts should update. entry: &nflogpb.Entry{FiringAlerts: []uint64{1, 2, 3}}, firingAlerts: alertHashSet(2, 3, 4), res: true, }, { // Zero timestamp in the nflog entry should always update. entry: &nflogpb.Entry{ FiringAlerts: []uint64{1, 2, 3}, Timestamp: time.Time{}, }, firingAlerts: alertHashSet(1, 2, 3), res: true, }, { // Identical sets of alerts shouldn't update before repeat_interval. entry: &nflogpb.Entry{ FiringAlerts: []uint64{1, 2, 3}, Timestamp: now.Add(-9 * time.Minute), }, repeat: 10 * time.Minute, firingAlerts: alertHashSet(1, 2, 3), res: false, }, { // Identical sets of alerts should update after repeat_interval. entry: &nflogpb.Entry{ FiringAlerts: []uint64{1, 2, 3}, Timestamp: now.Add(-11 * time.Minute), }, repeat: 10 * time.Minute, firingAlerts: alertHashSet(1, 2, 3), res: true, }, { // Different sets of resolved alerts without firing alerts shouldn't update after repeat_interval. entry: &nflogpb.Entry{ ResolvedAlerts: []uint64{1, 2, 3}, Timestamp: now.Add(-11 * time.Minute), }, repeat: 10 * time.Minute, resolvedAlerts: alertHashSet(3, 4, 5), resolve: true, res: false, }, { // Different sets of resolved alerts shouldn't update when resolve is false. entry: &nflogpb.Entry{ FiringAlerts: []uint64{1, 2}, ResolvedAlerts: []uint64{3}, Timestamp: now.Add(-9 * time.Minute), }, repeat: 10 * time.Minute, firingAlerts: alertHashSet(1), resolvedAlerts: alertHashSet(2, 3), resolve: false, res: false, }, { // Different sets of resolved alerts should update when resolve is true. entry: &nflogpb.Entry{ FiringAlerts: []uint64{1, 2}, ResolvedAlerts: []uint64{3}, Timestamp: now.Add(-9 * time.Minute), }, repeat: 10 * time.Minute, firingAlerts: alertHashSet(1), resolvedAlerts: alertHashSet(2, 3), resolve: true, res: true, }, { // Empty set of firing alerts should update when resolve is false. entry: &nflogpb.Entry{ FiringAlerts: []uint64{1, 2}, ResolvedAlerts: []uint64{3}, Timestamp: now.Add(-9 * time.Minute), }, repeat: 10 * time.Minute, firingAlerts: alertHashSet(), resolvedAlerts: alertHashSet(1, 2, 3), resolve: false, res: true, }, { // Empty set of firing alerts should update when resolve is true. entry: &nflogpb.Entry{ FiringAlerts: []uint64{1, 2}, ResolvedAlerts: []uint64{3}, Timestamp: now.Add(-9 * time.Minute), }, repeat: 10 * time.Minute, firingAlerts: alertHashSet(), resolvedAlerts: alertHashSet(1, 2, 3), resolve: true, res: true, }, } for i, c := range cases { t.Log("case", i) s := &DedupStage{ now: func() time.Time { return now }, conf: notifierConfigFunc(func() bool { return c.resolve }), } res := s.needsUpdate(c.entry, c.firingAlerts, c.resolvedAlerts, c.repeat) require.Equal(t, c.res, res) } } func TestDedupStage(t *testing.T) { i := 0 now := utcNow() s := &DedupStage{ hash: func(a *types.Alert) uint64 { res := uint64(i) i++ return res }, now: func() time.Time { return now }, conf: notifierConfigFunc(func() bool { return false }), } ctx := context.Background() _, _, err := s.Exec(ctx, log.NewNopLogger()) require.EqualError(t, err, "group key missing") ctx = WithGroupKey(ctx, "1") _, _, err = s.Exec(ctx, log.NewNopLogger()) require.EqualError(t, err, "repeat interval missing") ctx = WithRepeatInterval(ctx, time.Hour) alerts := []*types.Alert{{}, {}, {}} // Must catch notification log query errors. s.nflog = &testNflog{ qerr: errors.New("bad things"), } ctx, _, err = s.Exec(ctx, log.NewNopLogger(), alerts...) require.EqualError(t, err, "bad things") // ... but skip ErrNotFound. s.nflog = &testNflog{ qerr: nflog.ErrNotFound, } ctx, res, err := s.Exec(ctx, log.NewNopLogger(), alerts...) require.NoError(t, err, "unexpected error on not found log entry") require.Equal(t, alerts, res, "input alerts differ from result alerts") s.nflog = &testNflog{ qerr: nil, qres: []*nflogpb.Entry{ {FiringAlerts: []uint64{0, 1, 2}}, {FiringAlerts: []uint64{1, 2, 3}}, }, } ctx, _, err = s.Exec(ctx, log.NewNopLogger(), alerts...) require.Contains(t, err.Error(), "result size") // Must return no error and no alerts no need to update. i = 0 s.nflog = &testNflog{ qerr: nflog.ErrNotFound, qres: []*nflogpb.Entry{ { FiringAlerts: []uint64{0, 1, 2}, Timestamp: now, }, }, } ctx, res, err = s.Exec(ctx, log.NewNopLogger(), alerts...) require.NoError(t, err) require.Nil(t, res, "unexpected alerts returned") // Must return no error and all input alerts on changes. i = 0 s.nflog = &testNflog{ qerr: nil, qres: []*nflogpb.Entry{ { FiringAlerts: []uint64{1, 2, 3, 4}, Timestamp: now, }, }, } _, res, err = s.Exec(ctx, log.NewNopLogger(), alerts...) require.NoError(t, err) require.Equal(t, alerts, res, "unexpected alerts returned") } func TestMultiStage(t *testing.T) { var ( alerts1 = []*types.Alert{{}} alerts2 = []*types.Alert{{}, {}} alerts3 = []*types.Alert{{}, {}, {}} ) stage := MultiStage{ StageFunc(func(ctx context.Context, l log.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error) { if !reflect.DeepEqual(alerts, alerts1) { t.Fatal("Input not equal to input of MultiStage") } ctx = context.WithValue(ctx, "key", "value") return ctx, alerts2, nil }), StageFunc(func(ctx context.Context, l log.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error) { if !reflect.DeepEqual(alerts, alerts2) { t.Fatal("Input not equal to output of previous stage") } v, ok := ctx.Value("key").(string) if !ok || v != "value" { t.Fatalf("Expected value %q for key %q but got %q", "value", "key", v) } return ctx, alerts3, nil }), } _, alerts, err := stage.Exec(context.Background(), log.NewNopLogger(), alerts1...) if err != nil { t.Fatalf("Exec failed: %s", err) } if !reflect.DeepEqual(alerts, alerts3) { t.Fatal("Output of MultiStage is not equal to the output of the last stage") } } func TestMultiStageFailure(t *testing.T) { var ( ctx = context.Background() s1 = failStage{} stage = MultiStage{s1} ) _, _, err := stage.Exec(ctx, log.NewNopLogger(), nil) if err.Error() != "some error" { t.Fatal("Errors were not propagated correctly by MultiStage") } } func TestRoutingStage(t *testing.T) { var ( alerts1 = []*types.Alert{{}} alerts2 = []*types.Alert{{}, {}} ) stage := RoutingStage{ "name": StageFunc(func(ctx context.Context, l log.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error) { if !reflect.DeepEqual(alerts, alerts1) { t.Fatal("Input not equal to input of RoutingStage") } return ctx, alerts2, nil }), "not": failStage{}, } ctx := WithReceiverName(context.Background(), "name") _, alerts, err := stage.Exec(ctx, log.NewNopLogger(), alerts1...) if err != nil { t.Fatalf("Exec failed: %s", err) } if !reflect.DeepEqual(alerts, alerts2) { t.Fatal("Output of RoutingStage is not equal to the output of the inner stage") } } func TestRetryStageWithError(t *testing.T) { fail, retry := true, true sent := []*types.Alert{} i := Integration{ notifier: notifierFunc(func(ctx context.Context, alerts ...*types.Alert) (bool, error) { if fail { fail = false return retry, errors.New("fail to deliver notification") } sent = append(sent, alerts...) return false, nil }), conf: notifierConfigFunc(func() bool { return false }), } r := RetryStage{ integration: i, } alerts := []*types.Alert{ &types.Alert{ Alert: model.Alert{ EndsAt: time.Now().Add(time.Hour), }, }, } ctx := context.Background() ctx = WithFiringAlerts(ctx, []uint64{0}) // Notify with a recoverable error should retry and succeed. resctx, res, err := r.Exec(ctx, log.NewNopLogger(), alerts...) require.Nil(t, err) require.Equal(t, alerts, res) require.Equal(t, alerts, sent) require.NotNil(t, resctx) // Notify with an unrecoverable error should fail. sent = sent[:0] fail = true retry = false resctx, _, err = r.Exec(ctx, log.NewNopLogger(), alerts...) require.NotNil(t, err) require.NotNil(t, resctx) } func TestRetryStageNoResolved(t *testing.T) { sent := []*types.Alert{} i := Integration{ notifier: notifierFunc(func(ctx context.Context, alerts ...*types.Alert) (bool, error) { sent = append(sent, alerts...) return false, nil }), conf: notifierConfigFunc(func() bool { return false }), } r := RetryStage{ integration: i, } alerts := []*types.Alert{ &types.Alert{ Alert: model.Alert{ EndsAt: time.Now().Add(-time.Hour), }, }, &types.Alert{ Alert: model.Alert{ EndsAt: time.Now().Add(time.Hour), }, }, } ctx := context.Background() resctx, res, err := r.Exec(ctx, log.NewNopLogger(), alerts...) require.EqualError(t, err, "firing alerts missing") require.Nil(t, res) require.NotNil(t, resctx) ctx = WithFiringAlerts(ctx, []uint64{0}) resctx, res, err = r.Exec(ctx, log.NewNopLogger(), alerts...) require.Nil(t, err) require.Equal(t, alerts, res) require.Equal(t, []*types.Alert{alerts[1]}, sent) require.NotNil(t, resctx) // All alerts are resolved. sent = sent[:0] ctx = WithFiringAlerts(ctx, []uint64{}) alerts[1].Alert.EndsAt = time.Now().Add(-time.Hour) resctx, res, err = r.Exec(ctx, log.NewNopLogger(), alerts...) require.Nil(t, err) require.Equal(t, alerts, res) require.Equal(t, []*types.Alert{}, sent) require.NotNil(t, resctx) } func TestRetryStageSendResolved(t *testing.T) { sent := []*types.Alert{} i := Integration{ notifier: notifierFunc(func(ctx context.Context, alerts ...*types.Alert) (bool, error) { sent = append(sent, alerts...) return false, nil }), conf: notifierConfigFunc(func() bool { return true }), } r := RetryStage{ integration: i, } alerts := []*types.Alert{ &types.Alert{ Alert: model.Alert{ EndsAt: time.Now().Add(-time.Hour), }, }, &types.Alert{ Alert: model.Alert{ EndsAt: time.Now().Add(time.Hour), }, }, } ctx := context.Background() ctx = WithFiringAlerts(ctx, []uint64{0}) resctx, res, err := r.Exec(ctx, log.NewNopLogger(), alerts...) require.Nil(t, err) require.Equal(t, alerts, res) require.Equal(t, alerts, sent) require.NotNil(t, resctx) // All alerts are resolved. sent = sent[:0] ctx = WithFiringAlerts(ctx, []uint64{}) alerts[1].Alert.EndsAt = time.Now().Add(-time.Hour) resctx, res, err = r.Exec(ctx, log.NewNopLogger(), alerts...) require.Nil(t, err) require.Equal(t, alerts, res) require.Equal(t, alerts, sent) require.NotNil(t, resctx) } func TestSetNotifiesStage(t *testing.T) { tnflog := &testNflog{} s := &SetNotifiesStage{ recv: &nflogpb.Receiver{GroupName: "test"}, nflog: tnflog, } alerts := []*types.Alert{{}, {}, {}} ctx := context.Background() resctx, res, err := s.Exec(ctx, log.NewNopLogger(), alerts...) require.EqualError(t, err, "group key missing") require.Nil(t, res) require.NotNil(t, resctx) ctx = WithGroupKey(ctx, "1") resctx, res, err = s.Exec(ctx, log.NewNopLogger(), alerts...) require.EqualError(t, err, "firing alerts missing") require.Nil(t, res) require.NotNil(t, resctx) ctx = WithFiringAlerts(ctx, []uint64{0, 1, 2}) resctx, res, err = s.Exec(ctx, log.NewNopLogger(), alerts...) require.EqualError(t, err, "resolved alerts missing") require.Nil(t, res) require.NotNil(t, resctx) ctx = WithResolvedAlerts(ctx, []uint64{}) tnflog.logFunc = func(r *nflogpb.Receiver, gkey string, firingAlerts, resolvedAlerts []uint64) error { require.Equal(t, s.recv, r) require.Equal(t, "1", gkey) require.Equal(t, []uint64{0, 1, 2}, firingAlerts) require.Equal(t, []uint64{}, resolvedAlerts) return nil } resctx, res, err = s.Exec(ctx, log.NewNopLogger(), alerts...) require.Nil(t, err) require.Equal(t, alerts, res) require.NotNil(t, resctx) ctx = WithFiringAlerts(ctx, []uint64{}) ctx = WithResolvedAlerts(ctx, []uint64{0, 1, 2}) tnflog.logFunc = func(r *nflogpb.Receiver, gkey string, firingAlerts, resolvedAlerts []uint64) error { require.Equal(t, s.recv, r) require.Equal(t, "1", gkey) require.Equal(t, []uint64{}, firingAlerts) require.Equal(t, []uint64{0, 1, 2}, resolvedAlerts) return nil } resctx, res, err = s.Exec(ctx, log.NewNopLogger(), alerts...) require.Nil(t, err) require.Equal(t, alerts, res) require.NotNil(t, resctx) } func TestSilenceStage(t *testing.T) { silences, err := silence.New(silence.Options{}) if err != nil { t.Fatal(err) } if _, err := silences.Set(&silencepb.Silence{ EndsAt: utcNow().Add(time.Hour), Matchers: []*silencepb.Matcher{{Name: "mute", Pattern: "me"}}, }); err != nil { t.Fatal(err) } marker := types.NewMarker() silencer := NewSilenceStage(silences, marker) in := []model.LabelSet{ {}, {"test": "set"}, {"mute": "me"}, {"foo": "bar", "test": "set"}, {"foo": "bar", "mute": "me"}, {}, {"not": "muted"}, } out := []model.LabelSet{ {}, {"test": "set"}, {"foo": "bar", "test": "set"}, {}, {"not": "muted"}, } var inAlerts []*types.Alert for _, lset := range in { inAlerts = append(inAlerts, &types.Alert{ Alert: model.Alert{Labels: lset}, }) } // Set the second alert as previously silenced. It is expected to have // the WasSilenced flag set to true afterwards. marker.SetSilenced(inAlerts[1].Fingerprint(), "123") _, alerts, err := silencer.Exec(nil, log.NewNopLogger(), inAlerts...) if err != nil { t.Fatalf("Exec failed: %s", err) } var got []model.LabelSet for _, a := range alerts { got = append(got, a.Labels) } if !reflect.DeepEqual(got, out) { t.Fatalf("Muting failed, expected: %v\ngot %v", out, got) } } func TestInhibitStage(t *testing.T) { // Mute all label sets that have a "mute" key. muter := types.MuteFunc(func(lset model.LabelSet) bool { _, ok := lset["mute"] return ok }) inhibitor := NewInhibitStage(muter) in := []model.LabelSet{ {}, {"test": "set"}, {"mute": "me"}, {"foo": "bar", "test": "set"}, {"foo": "bar", "mute": "me"}, {}, {"not": "muted"}, } out := []model.LabelSet{ {}, {"test": "set"}, {"foo": "bar", "test": "set"}, {}, {"not": "muted"}, } var inAlerts []*types.Alert for _, lset := range in { inAlerts = append(inAlerts, &types.Alert{ Alert: model.Alert{Labels: lset}, }) } _, alerts, err := inhibitor.Exec(nil, log.NewNopLogger(), inAlerts...) if err != nil { t.Fatalf("Exec failed: %s", err) } var got []model.LabelSet for _, a := range alerts { got = append(got, a.Labels) } if !reflect.DeepEqual(got, out) { t.Fatalf("Muting failed, expected: %v\ngot %v", out, got) } } prometheus-alertmanager-0.15.3+ds/pkg/000077500000000000000000000000001341674552200176275ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/pkg/parse/000077500000000000000000000000001341674552200207415ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/pkg/parse/parse.go000066400000000000000000000044221341674552200224040ustar00rootroot00000000000000// Copyright 2018 Prometheus Team // 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 parse import ( "fmt" "regexp" "strings" "github.com/prometheus/prometheus/pkg/labels" ) var ( re = regexp.MustCompile(`(?:\s?)(\w+)(=|=~|!=|!~)(?:\"([^"=~!]+)\"|([^"=~!]+)|\"\")`) typeMap = map[string]labels.MatchType{ "=": labels.MatchEqual, "!=": labels.MatchNotEqual, "=~": labels.MatchRegexp, "!~": labels.MatchNotRegexp, } ) func Matchers(s string) ([]*labels.Matcher, error) { matchers := []*labels.Matcher{} if strings.HasPrefix(s, "{") { s = s[1:] } if strings.HasSuffix(s, "}") { s = s[:len(s)-1] } var insideQuotes bool var token string var tokens []string for _, r := range s { if !insideQuotes && r == ',' { tokens = append(tokens, token) token = "" continue } token += string(r) if r == '"' { insideQuotes = !insideQuotes } } if token != "" { tokens = append(tokens, token) } for _, token := range tokens { m, err := Matcher(token) if err != nil { return nil, err } matchers = append(matchers, m) } return matchers, nil } func Matcher(s string) (*labels.Matcher, error) { name, value, matchType, err := Input(s) if err != nil { return nil, err } m, err := labels.NewMatcher(matchType, name, value) if err != nil { return nil, err } return m, nil } func Input(s string) (name, value string, matchType labels.MatchType, err error) { ms := re.FindStringSubmatch(s) if len(ms) < 4 { return "", "", labels.MatchEqual, fmt.Errorf("bad matcher format: %s", s) } var prs bool name = ms[1] matchType, prs = typeMap[ms[2]] if ms[3] != "" { value = ms[3] } else { value = ms[4] } if name == "" || !prs { return "", "", labels.MatchEqual, fmt.Errorf("failed to parse") } return name, value, matchType, nil } prometheus-alertmanager-0.15.3+ds/pkg/parse/parse_test.go000066400000000000000000000103641341674552200234450ustar00rootroot00000000000000// Copyright 2018 Prometheus Team // 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 parse import ( "reflect" "testing" "github.com/prometheus/prometheus/pkg/labels" ) func TestMatchers(t *testing.T) { testCases := []struct { input string want []*labels.Matcher err error }{ { input: `{foo="bar"}`, want: func() []*labels.Matcher { ms := []*labels.Matcher{} m, _ := labels.NewMatcher(labels.MatchEqual, "foo", "bar") return append(ms, m) }(), }, { input: `{foo=~"bar.*"}`, want: func() []*labels.Matcher { ms := []*labels.Matcher{} m, _ := labels.NewMatcher(labels.MatchRegexp, "foo", "bar.*") return append(ms, m) }(), }, { input: `{foo!="bar"}`, want: func() []*labels.Matcher { ms := []*labels.Matcher{} m, _ := labels.NewMatcher(labels.MatchNotEqual, "foo", "bar") return append(ms, m) }(), }, { input: `{foo!~"bar.*"}`, want: func() []*labels.Matcher { ms := []*labels.Matcher{} m, _ := labels.NewMatcher(labels.MatchNotRegexp, "foo", "bar.*") return append(ms, m) }(), }, { input: `{foo="bar", baz!="quux"}`, want: func() []*labels.Matcher { ms := []*labels.Matcher{} m, _ := labels.NewMatcher(labels.MatchEqual, "foo", "bar") m2, _ := labels.NewMatcher(labels.MatchNotEqual, "baz", "quux") return append(ms, m, m2) }(), }, { input: `{foo="bar", baz!~"quux.*"}`, want: func() []*labels.Matcher { ms := []*labels.Matcher{} m, _ := labels.NewMatcher(labels.MatchEqual, "foo", "bar") m2, _ := labels.NewMatcher(labels.MatchNotRegexp, "baz", "quux.*") return append(ms, m, m2) }(), }, { input: `{foo="bar",baz!~".*quux", derp="wat"}`, want: func() []*labels.Matcher { ms := []*labels.Matcher{} m, _ := labels.NewMatcher(labels.MatchEqual, "foo", "bar") m2, _ := labels.NewMatcher(labels.MatchNotRegexp, "baz", ".*quux") m3, _ := labels.NewMatcher(labels.MatchEqual, "derp", "wat") return append(ms, m, m2, m3) }(), }, { input: `{foo="bar", baz!="quux", derp="wat"}`, want: func() []*labels.Matcher { ms := []*labels.Matcher{} m, _ := labels.NewMatcher(labels.MatchEqual, "foo", "bar") m2, _ := labels.NewMatcher(labels.MatchNotEqual, "baz", "quux") m3, _ := labels.NewMatcher(labels.MatchEqual, "derp", "wat") return append(ms, m, m2, m3) }(), }, { input: `{foo="bar", baz!~".*quux.*", derp="wat"}`, want: func() []*labels.Matcher { ms := []*labels.Matcher{} m, _ := labels.NewMatcher(labels.MatchEqual, "foo", "bar") m2, _ := labels.NewMatcher(labels.MatchNotRegexp, "baz", ".*quux.*") m3, _ := labels.NewMatcher(labels.MatchEqual, "derp", "wat") return append(ms, m, m2, m3) }(), }, { input: `{foo="bar", instance=~"some-api.*"}`, want: func() []*labels.Matcher { ms := []*labels.Matcher{} m, _ := labels.NewMatcher(labels.MatchEqual, "foo", "bar") m2, _ := labels.NewMatcher(labels.MatchRegexp, "instance", "some-api.*") return append(ms, m, m2) }(), }, { input: `{foo=""}`, want: func() []*labels.Matcher { ms := []*labels.Matcher{} m, _ := labels.NewMatcher(labels.MatchEqual, "foo", "") return append(ms, m) }(), }, { input: `{foo="bar,quux", job="job1"}`, want: func() []*labels.Matcher { ms := []*labels.Matcher{} m, _ := labels.NewMatcher(labels.MatchEqual, "foo", "bar,quux") m2, _ := labels.NewMatcher(labels.MatchEqual, "job", "job1") return append(ms, m, m2) }(), }, } for i, tc := range testCases { got, err := Matchers(tc.input) if tc.err != err { t.Fatalf("error not equal (i=%d):\ngot %v\nwant %v", i, err, tc.err) } if !reflect.DeepEqual(got, tc.want) { t.Fatalf("labels not equal (i=%d):\ngot %v\nwant %v", i, got, tc.want) } } } prometheus-alertmanager-0.15.3+ds/provider/000077500000000000000000000000001341674552200207005ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/provider/mem/000077500000000000000000000000001341674552200214565ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/provider/mem/mem.go000066400000000000000000000103611341674552200225640ustar00rootroot00000000000000// Copyright 2016 Prometheus Team // 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/lic:wenses/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 mem import ( "sync" "time" "github.com/prometheus/alertmanager/provider" "github.com/prometheus/alertmanager/types" "github.com/prometheus/common/model" ) // Alerts gives access to a set of alerts. All methods are goroutine-safe. type Alerts struct { mtx sync.RWMutex alerts map[model.Fingerprint]*types.Alert marker types.Marker intervalGC time.Duration stopGC chan struct{} listeners map[int]listeningAlerts next int } type listeningAlerts struct { alerts chan *types.Alert done chan struct{} } // NewAlerts returns a new alert provider. func NewAlerts(m types.Marker, intervalGC time.Duration) (*Alerts, error) { a := &Alerts{ alerts: map[model.Fingerprint]*types.Alert{}, marker: m, intervalGC: intervalGC, stopGC: make(chan struct{}), listeners: map[int]listeningAlerts{}, next: 0, } go a.runGC() return a, nil } func (a *Alerts) runGC() { for { select { case <-a.stopGC: return case <-time.After(a.intervalGC): } a.mtx.Lock() for fp, alert := range a.alerts { // As we don't persist alerts, we no longer consider them after // they are resolved. Alerts waiting for resolved notifications are // held in memory in aggregation groups redundantly. if alert.EndsAt.Before(time.Now()) { delete(a.alerts, fp) a.marker.Delete(fp) } } a.mtx.Unlock() } } // Close the alert provider. func (a *Alerts) Close() error { close(a.stopGC) return nil } // Subscribe returns an iterator over active alerts that have not been // resolved and successfully notified about. // They are not guaranteed to be in chronological order. func (a *Alerts) Subscribe() provider.AlertIterator { var ( ch = make(chan *types.Alert, 200) done = make(chan struct{}) ) alerts, err := a.getPending() a.mtx.Lock() i := a.next a.next++ a.listeners[i] = listeningAlerts{alerts: ch, done: done} a.mtx.Unlock() go func() { defer func() { a.mtx.Lock() delete(a.listeners, i) close(ch) a.mtx.Unlock() }() for _, a := range alerts { select { case ch <- a: case <-done: return } } <-done }() return provider.NewAlertIterator(ch, done, err) } // GetPending returns an iterator over all alerts that have // pending notifications. func (a *Alerts) GetPending() provider.AlertIterator { var ( ch = make(chan *types.Alert, 200) done = make(chan struct{}) ) alerts, err := a.getPending() go func() { defer close(ch) for _, a := range alerts { select { case ch <- a: case <-done: return } } }() return provider.NewAlertIterator(ch, done, err) } func (a *Alerts) getPending() ([]*types.Alert, error) { a.mtx.RLock() defer a.mtx.RUnlock() res := make([]*types.Alert, 0, len(a.alerts)) for _, alert := range a.alerts { res = append(res, alert) } return res, nil } // Get returns the alert for a given fingerprint. func (a *Alerts) Get(fp model.Fingerprint) (*types.Alert, error) { a.mtx.RLock() defer a.mtx.RUnlock() alert, ok := a.alerts[fp] if !ok { return nil, provider.ErrNotFound } return alert, nil } // Put adds the given alert to the set. func (a *Alerts) Put(alerts ...*types.Alert) error { a.mtx.Lock() defer a.mtx.Unlock() for _, alert := range alerts { fp := alert.Fingerprint() if old, ok := a.alerts[fp]; ok { // Merge alerts if there is an overlap in activity range. if (alert.EndsAt.After(old.StartsAt) && alert.EndsAt.Before(old.EndsAt)) || (alert.StartsAt.After(old.StartsAt) && alert.StartsAt.Before(old.EndsAt)) { alert = old.Merge(alert) } } a.alerts[fp] = alert for _, l := range a.listeners { select { case l.alerts <- alert: case <-l.done: } } } return nil } prometheus-alertmanager-0.15.3+ds/provider/mem/mem_test.go000066400000000000000000000150661341674552200236320ustar00rootroot00000000000000// Copyright 2016 Prometheus Team // 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/lic:wenses/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 mem import ( "fmt" "reflect" "testing" "time" "sync" "github.com/kylelemons/godebug/pretty" "github.com/prometheus/alertmanager/provider" "github.com/prometheus/alertmanager/types" "github.com/prometheus/common/model" ) var ( t0 = time.Now() t1 = t0.Add(100 * time.Millisecond) alert1 = &types.Alert{ Alert: model.Alert{ Labels: model.LabelSet{"bar": "foo"}, Annotations: model.LabelSet{"foo": "bar"}, StartsAt: t0, EndsAt: t1, GeneratorURL: "http://example.com/prometheus", }, UpdatedAt: t0, Timeout: false, } alert2 = &types.Alert{ Alert: model.Alert{ Labels: model.LabelSet{"bar": "foo2"}, Annotations: model.LabelSet{"foo": "bar2"}, StartsAt: t0, EndsAt: t1, GeneratorURL: "http://example.com/prometheus", }, UpdatedAt: t0, Timeout: false, } alert3 = &types.Alert{ Alert: model.Alert{ Labels: model.LabelSet{"bar": "foo3"}, Annotations: model.LabelSet{"foo": "bar3"}, StartsAt: t0, EndsAt: t1, GeneratorURL: "http://example.com/prometheus", }, UpdatedAt: t0, Timeout: false, } ) func init() { pretty.CompareConfig.IncludeUnexported = true } func TestAlertsPut(t *testing.T) { marker := types.NewMarker() alerts, err := NewAlerts(marker, 30*time.Minute) if err != nil { t.Fatal(err) } insert := []*types.Alert{alert1, alert2, alert3} if err := alerts.Put(insert...); err != nil { t.Fatalf("Insert failed: %s", err) } for i, a := range insert { res, err := alerts.Get(a.Fingerprint()) if err != nil { t.Fatalf("retrieval error: %s", err) } if !alertsEqual(res, a) { t.Errorf("Unexpected alert: %d", i) t.Fatalf(pretty.Compare(res, a)) } } } func TestAlertsSubscribe(t *testing.T) { marker := types.NewMarker() alerts, err := NewAlerts(marker, 30*time.Minute) if err != nil { t.Fatal(err) } // add alert1 to validate if pending alerts will be send if err := alerts.Put(alert1); err != nil { t.Fatalf("Insert failed: %s", err) } var wg sync.WaitGroup wg.Add(2) fatalc := make(chan string, 2) iterator1 := alerts.Subscribe() iterator2 := alerts.Subscribe() go func() { defer wg.Done() expectedAlerts := map[model.Fingerprint]*types.Alert{ alert1.Fingerprint(): alert1, alert2.Fingerprint(): alert2, alert3.Fingerprint(): alert3, } for i := 0; i < 3; i++ { actual := <-iterator1.Next() expected := expectedAlerts[actual.Fingerprint()] if !alertsEqual(actual, expected) { fatalc <- fmt.Sprintf("Unexpected alert (iterator1)\n%s", pretty.Compare(actual, expected)) return } delete(expectedAlerts, actual.Fingerprint()) } if len(expectedAlerts) != 0 { fatalc <- fmt.Sprintf("Unexpected number of alerts (iterator1): %d", len(expectedAlerts)) } }() go func() { defer wg.Done() expectedAlerts := map[model.Fingerprint]*types.Alert{ alert1.Fingerprint(): alert1, alert2.Fingerprint(): alert2, alert3.Fingerprint(): alert3, } for i := 0; i < 3; i++ { actual := <-iterator2.Next() expected := expectedAlerts[actual.Fingerprint()] if !alertsEqual(actual, expected) { t.Errorf("Unexpected alert") fatalc <- fmt.Sprintf("Unexpected alert (iterator2)\n%s", pretty.Compare(actual, expected)) } delete(expectedAlerts, actual.Fingerprint()) } if len(expectedAlerts) != 0 { fatalc <- fmt.Sprintf("Unexpected number of alerts (iterator2): %d", len(expectedAlerts)) } }() go func() { wg.Wait() close(fatalc) }() if err := alerts.Put(alert2); err != nil { t.Fatalf("Insert failed: %s", err) } if err := alerts.Put(alert3); err != nil { t.Fatalf("Insert failed: %s", err) } fatal, ok := <-fatalc if ok { t.Fatalf(fatal) } iterator1.Close() iterator2.Close() } func TestAlertsGetPending(t *testing.T) { marker := types.NewMarker() alerts, err := NewAlerts(marker, 30*time.Minute) if err != nil { t.Fatal(err) } if err := alerts.Put(alert1, alert2); err != nil { t.Fatalf("Insert failed: %s", err) } expectedAlerts := map[model.Fingerprint]*types.Alert{ alert1.Fingerprint(): alert1, alert2.Fingerprint(): alert2, } iterator := alerts.GetPending() for actual := range iterator.Next() { expected := expectedAlerts[actual.Fingerprint()] if !alertsEqual(actual, expected) { t.Errorf("Unexpected alert") t.Fatalf(pretty.Compare(actual, expected)) } } if err := alerts.Put(alert3); err != nil { t.Fatalf("Insert failed: %s", err) } expectedAlerts = map[model.Fingerprint]*types.Alert{ alert1.Fingerprint(): alert1, alert2.Fingerprint(): alert2, alert3.Fingerprint(): alert3, } iterator = alerts.GetPending() for actual := range iterator.Next() { expected := expectedAlerts[actual.Fingerprint()] if !alertsEqual(actual, expected) { t.Errorf("Unexpected alert") t.Fatalf(pretty.Compare(actual, expected)) } } } func TestAlertsGC(t *testing.T) { marker := types.NewMarker() alerts, err := NewAlerts(marker, 200*time.Millisecond) if err != nil { t.Fatal(err) } insert := []*types.Alert{alert1, alert2, alert3} if err := alerts.Put(insert...); err != nil { t.Fatalf("Insert failed: %s", err) } for _, a := range insert { marker.SetActive(a.Fingerprint()) if !marker.Active(a.Fingerprint()) { t.Errorf("error setting status: %v", a) } } time.Sleep(300 * time.Millisecond) for i, a := range insert { _, err := alerts.Get(a.Fingerprint()) if err != provider.ErrNotFound { t.Errorf("alert %d didn't get GC'd", i) } s := marker.Status(a.Fingerprint()) if s.State != types.AlertStateUnprocessed { t.Errorf("marker %d didn't get GC'd: %v", i, s) } } } func alertsEqual(a1, a2 *types.Alert) bool { if !reflect.DeepEqual(a1.Labels, a2.Labels) { return false } if !reflect.DeepEqual(a1.Annotations, a2.Annotations) { return false } if a1.GeneratorURL != a2.GeneratorURL { return false } if !a1.StartsAt.Equal(a2.StartsAt) { return false } if !a1.EndsAt.Equal(a2.EndsAt) { return false } if !a1.UpdatedAt.Equal(a2.UpdatedAt) { return false } return a1.Timeout == a2.Timeout } prometheus-alertmanager-0.15.3+ds/provider/provider.go000066400000000000000000000054721341674552200230710ustar00rootroot00000000000000// Copyright 2015 Prometheus Team // 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 provider import ( "fmt" "github.com/prometheus/common/model" "github.com/prometheus/alertmanager/types" ) var ( // ErrNotFound is returned if a provider cannot find a requested item. ErrNotFound = fmt.Errorf("item not found") ) // Iterator provides the functions common to all iterators. To be useful, a // specific iterator interface (e.g. AlertIterator) has to be implemented that // provides a Next method. type Iterator interface { // Err returns the current error. It is not safe to call it concurrently // with other iterator methods or while reading from a channel returned // by the iterator. Err() error // Close must be called to release resources once the iterator is not // used anymore. Close() } // AlertIterator is an Iterator for Alerts. type AlertIterator interface { Iterator // Next returns a channel that will be closed once the iterator is // exhausted. It is not necessary to exhaust the iterator but Close must // be called in any case to release resources used by the iterator (even // if the iterator is exhausted). Next() <-chan *types.Alert } // NewAlertIterator returns a new AlertIterator based on the generic alertIterator type func NewAlertIterator(ch <-chan *types.Alert, done chan struct{}, err error) AlertIterator { return &alertIterator{ ch: ch, done: done, err: err, } } // alertIterator implements AlertIterator. So far, this one fits all providers. type alertIterator struct { ch <-chan *types.Alert done chan struct{} err error } func (ai alertIterator) Next() <-chan *types.Alert { return ai.ch } func (ai alertIterator) Err() error { return ai.err } func (ai alertIterator) Close() { close(ai.done) } // Alerts gives access to a set of alerts. All methods are goroutine-safe. type Alerts interface { // Subscribe returns an iterator over active alerts that have not been // resolved and successfully notified about. // They are not guaranteed to be in chronological order. Subscribe() AlertIterator // GetPending returns an iterator over all alerts that have // pending notifications. GetPending() AlertIterator // Get returns the alert for a given fingerprint. Get(model.Fingerprint) (*types.Alert, error) // Put adds the given alert to the set. Put(...*types.Alert) error } prometheus-alertmanager-0.15.3+ds/scripts/000077500000000000000000000000001341674552200205355ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/scripts/genproto.sh000077500000000000000000000021321341674552200227270ustar00rootroot00000000000000#!/usr/bin/env bash # # Generate all protobuf bindings. # Run from repository root. # # Initial script taken from etcd under the Apache 2.0 license # File: https://github.com/coreos/etcd/blob/78a5eb79b510eb497deddd1a76f5153bc4b202d2/scripts/genproto.sh set -e set -u if ! [[ "$0" =~ "scripts/genproto.sh" ]]; then echo "must be run from repository root" exit 255 fi if ! [[ $(protoc --version) =~ "3.5.1" ]]; then echo "could not find protoc 3.5.1, is it installed + in PATH?" exit 255 fi GOGOPROTO_ROOT="${GOPATH}/src/github.com/gogo/protobuf" GOGOPROTO_PATH="${GOGOPROTO_ROOT}:${GOGOPROTO_ROOT}/protobuf" GRPC_GATEWAY_ROOT="${GOPATH}/src/github.com/grpc-ecosystem/grpc-gateway" DIRS="nflog/nflogpb silence/silencepb cluster/clusterpb" for dir in ${DIRS}; do pushd ${dir} protoc --gogofast_out=plugins=grpc:. -I=. \ -I="${GOGOPROTO_PATH}" \ -I="${GRPC_GATEWAY_ROOT}/third_party/googleapis" \ *.proto sed -i.bak -E 's/import _ \"gogoproto\"//g' *.pb.go sed -i.bak -E 's/import _ \"google\/protobuf\"//g' *.pb.go rm -f *.bak goimports -w *.pb.go popd done prometheus-alertmanager-0.15.3+ds/silence/000077500000000000000000000000001341674552200204705ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/silence/silence.go000066400000000000000000000502131341674552200224420ustar00rootroot00000000000000// Copyright 2016 Prometheus Team // 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 silence provides a storage for silences, which can share its // state over a mesh network and snapshot it. package silence import ( "bytes" "fmt" "io" "math/rand" "os" "reflect" "regexp" "sync" "time" "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" "github.com/matttproud/golang_protobuf_extensions/pbutil" "github.com/pkg/errors" "github.com/prometheus/alertmanager/cluster" pb "github.com/prometheus/alertmanager/silence/silencepb" "github.com/prometheus/alertmanager/types" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/common/model" "github.com/satori/go.uuid" ) // ErrNotFound is returned if a silence was not found. var ErrNotFound = fmt.Errorf("not found") // ErrInvalidState is returned if the state isn't valid. var ErrInvalidState = fmt.Errorf("invalid state") func utcNow() time.Time { return time.Now().UTC() } type matcherCache map[*pb.Silence]types.Matchers // Get retrieves the matchers for a given silence. If it is a missed cache // access, it compiles and adds the matchers of the requested silence to the // cache. func (c matcherCache) Get(s *pb.Silence) (types.Matchers, error) { if m, ok := c[s]; ok { return m, nil } return c.add(s) } // add compiles a silences' matchers and adds them to the cache. // It returns the compiled matchers. func (c matcherCache) add(s *pb.Silence) (types.Matchers, error) { var ( ms types.Matchers mt *types.Matcher ) for _, m := range s.Matchers { mt = &types.Matcher{ Name: m.Name, Value: m.Pattern, } switch m.Type { case pb.Matcher_EQUAL: mt.IsRegex = false case pb.Matcher_REGEXP: mt.IsRegex = true } err := mt.Init() if err != nil { return nil, err } ms = append(ms, mt) } c[s] = ms return ms, nil } // Silences holds a silence state that can be modified, queried, and snapshot. type Silences struct { logger log.Logger metrics *metrics now func() time.Time retention time.Duration mtx sync.RWMutex st state broadcast func([]byte) mc matcherCache } type metrics struct { gcDuration prometheus.Summary snapshotDuration prometheus.Summary snapshotSize prometheus.Gauge queriesTotal prometheus.Counter queryErrorsTotal prometheus.Counter queryDuration prometheus.Histogram silencesActive prometheus.GaugeFunc silencesPending prometheus.GaugeFunc silencesExpired prometheus.GaugeFunc propagatedMessagesTotal prometheus.Counter } func newSilenceMetricByState(s *Silences, st types.SilenceState) prometheus.GaugeFunc { return prometheus.NewGaugeFunc( prometheus.GaugeOpts{ Name: "alertmanager_silences", Help: "How many silences by state.", ConstLabels: prometheus.Labels{"state": string(st)}, }, func() float64 { count, err := s.CountState(st) if err != nil { level.Error(s.logger).Log("msg", "Counting silences failed", "err", err) } return float64(count) }, ) } func newMetrics(r prometheus.Registerer, s *Silences) *metrics { m := &metrics{} m.gcDuration = prometheus.NewSummary(prometheus.SummaryOpts{ Name: "alertmanager_silences_gc_duration_seconds", Help: "Duration of the last silence garbage collection cycle.", }) m.snapshotDuration = prometheus.NewSummary(prometheus.SummaryOpts{ Name: "alertmanager_silences_snapshot_duration_seconds", Help: "Duration of the last silence snapshot.", }) m.snapshotSize = prometheus.NewGauge(prometheus.GaugeOpts{ Name: "alertmanager_silences_snapshot_size_bytes", Help: "Size of the last silence snapshot in bytes.", }) m.queriesTotal = prometheus.NewCounter(prometheus.CounterOpts{ Name: "alertmanager_silences_queries_total", Help: "How many silence queries were received.", }) m.queryErrorsTotal = prometheus.NewCounter(prometheus.CounterOpts{ Name: "alertmanager_silences_query_errors_total", Help: "How many silence received queries did not succeed.", }) m.queryDuration = prometheus.NewHistogram(prometheus.HistogramOpts{ Name: "alertmanager_silences_query_duration_seconds", Help: "Duration of silence query evaluation.", }) m.propagatedMessagesTotal = prometheus.NewCounter(prometheus.CounterOpts{ Name: "alertmanager_silences_gossip_messages_propagated_total", Help: "Number of received gossip messages that have been further gossiped.", }) if s != nil { m.silencesActive = newSilenceMetricByState(s, types.SilenceStateActive) m.silencesPending = newSilenceMetricByState(s, types.SilenceStatePending) m.silencesExpired = newSilenceMetricByState(s, types.SilenceStateExpired) } if r != nil { r.MustRegister( m.gcDuration, m.snapshotDuration, m.snapshotSize, m.queriesTotal, m.queryErrorsTotal, m.queryDuration, m.silencesActive, m.silencesPending, m.silencesExpired, m.propagatedMessagesTotal, ) } return m } // Options exposes configuration options for creating a new Silences object. // Its zero value is a safe default. type Options struct { // A snapshot file or reader from which the initial state is loaded. // None or only one of them must be set. SnapshotFile string SnapshotReader io.Reader // Retention time for newly created Silences. Silences may be // garbage collected after the given duration after they ended. Retention time.Duration // A logger used by background processing. Logger log.Logger Metrics prometheus.Registerer } func (o *Options) validate() error { if o.SnapshotFile != "" && o.SnapshotReader != nil { return fmt.Errorf("only one of SnapshotFile and SnapshotReader must be set") } return nil } // New returns a new Silences object with the given configuration. func New(o Options) (*Silences, error) { if err := o.validate(); err != nil { return nil, err } if o.SnapshotFile != "" { if r, err := os.Open(o.SnapshotFile); err != nil { if !os.IsNotExist(err) { return nil, err } } else { o.SnapshotReader = r } } s := &Silences{ mc: matcherCache{}, logger: log.NewNopLogger(), retention: o.Retention, now: utcNow, broadcast: func([]byte) {}, st: state{}, } s.metrics = newMetrics(o.Metrics, s) if o.Logger != nil { s.logger = o.Logger } if o.SnapshotReader != nil { if err := s.loadSnapshot(o.SnapshotReader); err != nil { return s, err } } return s, nil } // Maintenance garbage collects the silence state at the given interval. If the snapshot // file is set, a snapshot is written to it afterwards. // Terminates on receiving from stopc. func (s *Silences) Maintenance(interval time.Duration, snapf string, stopc <-chan struct{}) { t := time.NewTicker(interval) defer t.Stop() f := func() error { start := s.now() var size int64 level.Debug(s.logger).Log("msg", "Running maintenance") defer func() { level.Debug(s.logger).Log("msg", "Maintenance done", "duration", s.now().Sub(start), "size", size) s.metrics.snapshotSize.Set(float64(size)) }() if _, err := s.GC(); err != nil { return err } if snapf == "" { return nil } f, err := openReplace(snapf) if err != nil { return err } if size, err = s.Snapshot(f); err != nil { return err } return f.Close() } Loop: for { select { case <-stopc: break Loop case <-t.C: if err := f(); err != nil { level.Info(s.logger).Log("msg", "Running maintenance failed", "err", err) } } } // No need for final maintenance if we don't want to snapshot. if snapf == "" { return } if err := f(); err != nil { level.Info(s.logger).Log("msg", "Creating shutdown snapshot failed", "err", err) } } // GC runs a garbage collection that removes silences that have ended longer // than the configured retention time ago. func (s *Silences) GC() (int, error) { start := time.Now() defer func() { s.metrics.gcDuration.Observe(time.Since(start).Seconds()) }() now := s.now() var n int s.mtx.Lock() defer s.mtx.Unlock() for id, sil := range s.st { if sil.ExpiresAt.IsZero() { return n, errors.New("unexpected zero expiration timestamp") } if !sil.ExpiresAt.After(now) { delete(s.st, id) delete(s.mc, sil.Silence) n++ } } return n, nil } func validateMatcher(m *pb.Matcher) error { if !model.LabelName(m.Name).IsValid() { return fmt.Errorf("invalid label name %q", m.Name) } switch m.Type { case pb.Matcher_EQUAL: if !model.LabelValue(m.Pattern).IsValid() { return fmt.Errorf("invalid label value %q", m.Pattern) } case pb.Matcher_REGEXP: if _, err := regexp.Compile(m.Pattern); err != nil { return fmt.Errorf("invalid regular expression %q: %s", m.Pattern, err) } default: return fmt.Errorf("unknown matcher type %q", m.Type) } return nil } func validateSilence(s *pb.Silence) error { if s.Id == "" { return errors.New("ID missing") } if len(s.Matchers) == 0 { return errors.New("at least one matcher required") } for i, m := range s.Matchers { if err := validateMatcher(m); err != nil { return fmt.Errorf("invalid label matcher %d: %s", i, err) } } if s.StartsAt.IsZero() { return errors.New("invalid zero start timestamp") } if s.EndsAt.IsZero() { return errors.New("invalid zero end timestamp") } if s.EndsAt.Before(s.StartsAt) { return errors.New("end time must not be before start time") } if s.UpdatedAt.IsZero() { return errors.New("invalid zero update timestamp") } return nil } // cloneSilence returns a shallow copy of a silence. func cloneSilence(sil *pb.Silence) *pb.Silence { s := *sil return &s } func (s *Silences) getSilence(id string) (*pb.Silence, bool) { msil, ok := s.st[id] if !ok { return nil, false } return msil.Silence, true } func (s *Silences) setSilence(sil *pb.Silence) error { sil.UpdatedAt = s.now() if err := validateSilence(sil); err != nil { return errors.Wrap(err, "silence invalid") } msil := &pb.MeshSilence{ Silence: sil, ExpiresAt: sil.EndsAt.Add(s.retention), } b, err := marshalMeshSilence(msil) if err != nil { return err } s.st.merge(msil) s.broadcast(b) return nil } // Set the specified silence. If a silence with the ID already exists and the modification // modifies history, the old silence gets expired and a new one is created. func (s *Silences) Set(sil *pb.Silence) (string, error) { s.mtx.Lock() defer s.mtx.Unlock() now := s.now() prev, ok := s.getSilence(sil.Id) if sil.Id != "" && !ok { return "", ErrNotFound } if ok { if canUpdate(prev, sil, now) { return sil.Id, s.setSilence(sil) } if getState(prev, s.now()) != types.SilenceStateExpired { // We cannot update the silence, expire the old one. if err := s.expire(prev.Id); err != nil { return "", errors.Wrap(err, "expire previous silence") } } } // If we got here it's either a new silence or a replacing one. sil.Id = uuid.NewV4().String() if sil.StartsAt.Before(now) { sil.StartsAt = now } return sil.Id, s.setSilence(sil) } // canUpdate returns true if silence a can be updated to b without // affecting the historic view of silencing. func canUpdate(a, b *pb.Silence, now time.Time) bool { if !reflect.DeepEqual(a.Matchers, b.Matchers) { return false } // Allowed timestamp modifications depend on the current time. switch st := getState(a, now); st { case types.SilenceStateActive: if !b.StartsAt.Equal(a.StartsAt) { return false } if b.EndsAt.Before(now) { return false } case types.SilenceStatePending: if b.StartsAt.Before(now) { return false } case types.SilenceStateExpired: return false default: panic("unknown silence state") } return true } // Expire the silence with the given ID immediately. func (s *Silences) Expire(id string) error { s.mtx.Lock() defer s.mtx.Unlock() return s.expire(id) } // Expire the silence with the given ID immediately. func (s *Silences) expire(id string) error { sil, ok := s.getSilence(id) if !ok { return ErrNotFound } sil = cloneSilence(sil) now := s.now() switch getState(sil, now) { case types.SilenceStateExpired: return errors.Errorf("silence %s already expired", id) case types.SilenceStateActive: sil.EndsAt = now case types.SilenceStatePending: // Set both to now to make Silence move to "expired" state sil.StartsAt = now sil.EndsAt = now } return s.setSilence(sil) } // QueryParam expresses parameters along which silences are queried. type QueryParam func(*query) error type query struct { ids []string filters []silenceFilter } // silenceFilter is a function that returns true if a silence // should be dropped from a result set for a given time. type silenceFilter func(*pb.Silence, *Silences, time.Time) (bool, error) var errNotSupported = errors.New("query parameter not supported") // QIDs configures a query to select the given silence IDs. func QIDs(ids ...string) QueryParam { return func(q *query) error { q.ids = append(q.ids, ids...) return nil } } // QTimeRange configures a query to search for silences that are active // in the given time range. // TODO(fabxc): not supported yet. func QTimeRange(start, end time.Time) QueryParam { return func(q *query) error { return errNotSupported } } // QMatches returns silences that match the given label set. func QMatches(set model.LabelSet) QueryParam { return func(q *query) error { f := func(sil *pb.Silence, s *Silences, _ time.Time) (bool, error) { m, err := s.mc.Get(sil) if err != nil { return true, err } return m.Match(set), nil } q.filters = append(q.filters, f) return nil } } // getState returns a silence's SilenceState at the given timestamp. func getState(sil *pb.Silence, ts time.Time) types.SilenceState { if ts.Before(sil.StartsAt) { return types.SilenceStatePending } if ts.After(sil.EndsAt) { return types.SilenceStateExpired } return types.SilenceStateActive } // QState filters queried silences by the given states. func QState(states ...types.SilenceState) QueryParam { return func(q *query) error { f := func(sil *pb.Silence, _ *Silences, now time.Time) (bool, error) { s := getState(sil, now) for _, ps := range states { if s == ps { return true, nil } } return false, nil } q.filters = append(q.filters, f) return nil } } // QueryOne queries with the given parameters and returns the first result. // Returns ErrNotFound if the query result is empty. func (s *Silences) QueryOne(params ...QueryParam) (*pb.Silence, error) { res, err := s.Query(params...) if err != nil { return nil, err } if len(res) == 0 { return nil, ErrNotFound } return res[0], nil } // Query for silences based on the given query parameters. func (s *Silences) Query(params ...QueryParam) ([]*pb.Silence, error) { start := time.Now() s.metrics.queriesTotal.Inc() sils, err := func() ([]*pb.Silence, error) { q := &query{} for _, p := range params { if err := p(q); err != nil { return nil, err } } return s.query(q, s.now()) }() if err != nil { s.metrics.queryErrorsTotal.Inc() } s.metrics.queryDuration.Observe(time.Since(start).Seconds()) return sils, err } // Count silences by state. func (s *Silences) CountState(states ...types.SilenceState) (int, error) { // This could probably be optimized. sils, err := s.Query(QState(states...)) if err != nil { return -1, err } return len(sils), nil } func (s *Silences) query(q *query, now time.Time) ([]*pb.Silence, error) { // If we have an ID constraint, all silences are our base set. // This and the use of post-filter functions is the // the trivial solution for now. var res []*pb.Silence s.mtx.Lock() defer s.mtx.Unlock() if q.ids != nil { for _, id := range q.ids { if s, ok := s.st[id]; ok { res = append(res, s.Silence) } } } else { for _, sil := range s.st { res = append(res, sil.Silence) } } var resf []*pb.Silence for _, sil := range res { remove := false for _, f := range q.filters { ok, err := f(sil, s, now) if err != nil { return nil, err } if !ok { remove = true break } } if !remove { resf = append(resf, cloneSilence(sil)) } } return resf, nil } // loadSnapshot loads a snapshot generated by Snapshot() into the state. // Any previous state is wiped. func (s *Silences) loadSnapshot(r io.Reader) error { st, err := decodeState(r) if err != nil { return err } for _, e := range st { // Comments list was moved to a single comment. Upgrade on loading the snapshot. if len(e.Silence.Comments) > 0 { e.Silence.Comment = e.Silence.Comments[0].Comment e.Silence.CreatedBy = e.Silence.Comments[0].Author e.Silence.Comments = nil } st[e.Silence.Id] = e } s.mtx.Lock() s.st = st s.mtx.Unlock() return nil } // Snapshot writes the full internal state into the writer and returns the number of bytes // written. func (s *Silences) Snapshot(w io.Writer) (int64, error) { start := time.Now() defer func() { s.metrics.snapshotDuration.Observe(time.Since(start).Seconds()) }() s.mtx.RLock() defer s.mtx.RUnlock() b, err := s.st.MarshalBinary() if err != nil { return 0, err } return io.Copy(w, bytes.NewReader(b)) } // MarshalBinary serializes all silences. func (s *Silences) MarshalBinary() ([]byte, error) { s.mtx.Lock() defer s.mtx.Unlock() return s.st.MarshalBinary() } // Merge merges silence state received from the cluster with the local state. func (s *Silences) Merge(b []byte) error { st, err := decodeState(bytes.NewReader(b)) if err != nil { return err } s.mtx.Lock() defer s.mtx.Unlock() for _, e := range st { if merged := s.st.merge(e); merged && !cluster.OversizedMessage(b) { // If this is the first we've seen the message and it's // not oversized, gossip it to other nodes. We don't // propagate oversized messages because they're sent to // all nodes already. s.broadcast(b) s.metrics.propagatedMessagesTotal.Inc() level.Debug(s.logger).Log("msg", "gossiping new silence", "silence", e) } } return nil } func (s *Silences) SetBroadcast(f func([]byte)) { s.mtx.Lock() s.broadcast = f s.mtx.Unlock() } type state map[string]*pb.MeshSilence func (s state) merge(e *pb.MeshSilence) bool { // Comments list was moved to a single comment. Apply upgrade // on silences received from peers. if len(e.Silence.Comments) > 0 { e.Silence.Comment = e.Silence.Comments[0].Comment e.Silence.CreatedBy = e.Silence.Comments[0].Author e.Silence.Comments = nil } id := e.Silence.Id prev, ok := s[id] if !ok || prev.Silence.UpdatedAt.Before(e.Silence.UpdatedAt) { s[id] = e return true } return false } func (s state) MarshalBinary() ([]byte, error) { var buf bytes.Buffer for _, e := range s { if _, err := pbutil.WriteDelimited(&buf, e); err != nil { return nil, err } } return buf.Bytes(), nil } func decodeState(r io.Reader) (state, error) { st := state{} for { var s pb.MeshSilence _, err := pbutil.ReadDelimited(r, &s) if err == nil { if s.Silence == nil { return nil, ErrInvalidState } st[s.Silence.Id] = &s continue } if err == io.EOF { break } return nil, err } return st, nil } func marshalMeshSilence(e *pb.MeshSilence) ([]byte, error) { var buf bytes.Buffer if _, err := pbutil.WriteDelimited(&buf, e); err != nil { return nil, err } return buf.Bytes(), nil } // replaceFile wraps a file that is moved to another filename on closing. type replaceFile struct { *os.File filename string } func (f *replaceFile) Close() error { if err := f.File.Sync(); err != nil { return err } if err := f.File.Close(); err != nil { return err } return os.Rename(f.File.Name(), f.filename) } // openReplace opens a new temporary file that is moved to filename on closing. func openReplace(filename string) (*replaceFile, error) { tmpFilename := fmt.Sprintf("%s.%x", filename, uint64(rand.Int63())) f, err := os.Create(tmpFilename) if err != nil { return nil, err } rf := &replaceFile{ File: f, filename: filename, } return rf, nil } prometheus-alertmanager-0.15.3+ds/silence/silence_test.go000066400000000000000000000604741341674552200235130ustar00rootroot00000000000000// Copyright 2016 Prometheus Team // 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 silence import ( "bytes" "io/ioutil" "os" "sort" "strings" "testing" "time" "github.com/matttproud/golang_protobuf_extensions/pbutil" pb "github.com/prometheus/alertmanager/silence/silencepb" "github.com/prometheus/alertmanager/types" "github.com/prometheus/common/model" "github.com/stretchr/testify/require" ) func TestOptionsValidate(t *testing.T) { cases := []struct { options *Options err string }{ { options: &Options{ SnapshotReader: &bytes.Buffer{}, }, }, { options: &Options{ SnapshotFile: "test.bkp", }, }, { options: &Options{ SnapshotFile: "test bkp", SnapshotReader: &bytes.Buffer{}, }, err: "only one of SnapshotFile and SnapshotReader must be set", }, } for _, c := range cases { err := c.options.validate() if err == nil { if c.err != "" { t.Errorf("expected error containing %q but got none", c.err) } continue } if err != nil && c.err == "" { t.Errorf("unexpected error %q", err) continue } if !strings.Contains(err.Error(), c.err) { t.Errorf("expected error to contain %q but got %q", c.err, err) } } } func TestSilencesGC(t *testing.T) { s, err := New(Options{}) require.NoError(t, err) now := utcNow() s.now = func() time.Time { return now } newSilence := func(exp time.Time) *pb.MeshSilence { return &pb.MeshSilence{ExpiresAt: exp} } s.st = state{ "1": newSilence(now), "2": newSilence(now.Add(-time.Second)), "3": newSilence(now.Add(time.Second)), } want := state{ "3": newSilence(now.Add(time.Second)), } n, err := s.GC() require.NoError(t, err) require.Equal(t, 2, n) require.Equal(t, want, s.st) } func TestSilencesSnapshot(t *testing.T) { // Check whether storing and loading the snapshot is symmetric. now := utcNow() cases := []struct { entries []*pb.MeshSilence }{ { entries: []*pb.MeshSilence{ { Silence: &pb.Silence{ Id: "3be80475-e219-4ee7-b6fc-4b65114e362f", Matchers: []*pb.Matcher{ {Name: "label1", Pattern: "val1", Type: pb.Matcher_EQUAL}, {Name: "label2", Pattern: "val.+", Type: pb.Matcher_REGEXP}, }, StartsAt: now, EndsAt: now, UpdatedAt: now, }, ExpiresAt: now, }, { Silence: &pb.Silence{ Id: "4b1e760d-182c-4980-b873-c1a6827c9817", Matchers: []*pb.Matcher{ {Name: "label1", Pattern: "val1", Type: pb.Matcher_EQUAL}, }, StartsAt: now.Add(time.Hour), EndsAt: now.Add(2 * time.Hour), UpdatedAt: now, }, ExpiresAt: now.Add(24 * time.Hour), }, }, }, } for _, c := range cases { f, err := ioutil.TempFile("", "snapshot") require.NoError(t, err, "creating temp file failed") s1 := &Silences{st: state{}, metrics: newMetrics(nil, nil)} // Setup internal state manually. for _, e := range c.entries { s1.st[e.Silence.Id] = e } _, err = s1.Snapshot(f) require.NoError(t, err, "creating snapshot failed") require.NoError(t, f.Close(), "closing snapshot file failed") f, err = os.Open(f.Name()) require.NoError(t, err, "opening snapshot file failed") // Check again against new nlog instance. s2 := &Silences{mc: matcherCache{}, st: state{}} err = s2.loadSnapshot(f) require.NoError(t, err, "error loading snapshot") require.Equal(t, s1.st, s2.st, "state after loading snapshot did not match snapshotted state") require.NoError(t, f.Close(), "closing snapshot file failed") } } func TestSilencesSetSilence(t *testing.T) { s, err := New(Options{ Retention: time.Minute, }) require.NoError(t, err) now := utcNow() nowpb := now sil := &pb.Silence{ Id: "some_id", Matchers: []*pb.Matcher{{Name: "abc", Pattern: "def"}}, StartsAt: nowpb, EndsAt: nowpb, } want := state{ "some_id": &pb.MeshSilence{ Silence: sil, ExpiresAt: now.Add(time.Minute), }, } done := make(chan bool) s.broadcast = func(b []byte) { var e pb.MeshSilence r := bytes.NewReader(b) _, err := pbutil.ReadDelimited(r, &e) require.NoError(t, err) require.Equal(t, want["some_id"], &e) close(done) } // setSilence() is always called with s.mtx locked() go func() { s.mtx.Lock() require.NoError(t, s.setSilence(sil)) s.mtx.Unlock() }() // GossipBroadcast is called in a goroutine. select { case <-done: case <-time.After(1 * time.Second): t.Fatal("GossipBroadcast was not called") } require.Equal(t, want, s.st, "Unexpected silence state") } func TestSilenceSet(t *testing.T) { s, err := New(Options{ Retention: time.Hour, }) require.NoError(t, err) now := utcNow() now1 := now s.now = func() time.Time { return now } // Insert silence with fixed start time. sil1 := &pb.Silence{ Matchers: []*pb.Matcher{{Name: "a", Pattern: "b"}}, StartsAt: now.Add(2 * time.Minute), EndsAt: now.Add(5 * time.Minute), } id1, err := s.Set(sil1) require.NoError(t, err) require.NotEqual(t, id1, "") want := state{ id1: &pb.MeshSilence{ Silence: &pb.Silence{ Id: id1, Matchers: []*pb.Matcher{{Name: "a", Pattern: "b"}}, StartsAt: now1.Add(2 * time.Minute), EndsAt: now1.Add(5 * time.Minute), UpdatedAt: now1, }, ExpiresAt: now1.Add(5*time.Minute + s.retention), }, } require.Equal(t, want, s.st, "unexpected state after silence creation") // Insert silence with unset start time. Must be set to now. now = now.Add(time.Minute) now2 := now sil2 := &pb.Silence{ Matchers: []*pb.Matcher{{Name: "a", Pattern: "b"}}, EndsAt: now.Add(1 * time.Minute), } id2, err := s.Set(sil2) require.NoError(t, err) require.NotEqual(t, id2, "") want = state{ id1: want[id1], id2: &pb.MeshSilence{ Silence: &pb.Silence{ Id: id2, Matchers: []*pb.Matcher{{Name: "a", Pattern: "b"}}, StartsAt: now2, EndsAt: now2.Add(1 * time.Minute), UpdatedAt: now2, }, ExpiresAt: now2.Add(1*time.Minute + s.retention), }, } require.Equal(t, want, s.st, "unexpected state after silence creation") // Overwrite silence 2 with new end time. now = now.Add(time.Minute) now3 := now sil3 := cloneSilence(sil2) sil3.EndsAt = now.Add(100 * time.Minute) id3, err := s.Set(sil3) require.NoError(t, err) require.Equal(t, id2, id3) want = state{ id1: want[id1], id2: &pb.MeshSilence{ Silence: &pb.Silence{ Id: id2, Matchers: []*pb.Matcher{{Name: "a", Pattern: "b"}}, StartsAt: now2, EndsAt: now3.Add(100 * time.Minute), UpdatedAt: now3, }, ExpiresAt: now3.Add(100*time.Minute + s.retention), }, } require.Equal(t, want, s.st, "unexpected state after silence creation") // Update silence 2 with new matcher expires it and creates a new one. now = now.Add(time.Minute) now4 := now sil4 := cloneSilence(sil3) sil4.Matchers = []*pb.Matcher{{Name: "a", Pattern: "c"}} id4, err := s.Set(sil4) require.NoError(t, err) require.NotEqual(t, id2, id4) want = state{ id1: want[id1], id2: &pb.MeshSilence{ Silence: &pb.Silence{ Id: id2, Matchers: []*pb.Matcher{{Name: "a", Pattern: "b"}}, StartsAt: now2, EndsAt: now4, UpdatedAt: now4, }, ExpiresAt: now4.Add(s.retention), }, id4: &pb.MeshSilence{ Silence: &pb.Silence{ Id: id4, Matchers: []*pb.Matcher{{Name: "a", Pattern: "c"}}, StartsAt: now4, EndsAt: now3.Add(100 * time.Minute), UpdatedAt: now4, }, ExpiresAt: now3.Add(100*time.Minute + s.retention), }, } require.Equal(t, want, s.st, "unexpected state after silence creation") // Re-create expired silence. now = now.Add(time.Minute) now5 := now sil5 := cloneSilence(sil3) sil5.StartsAt = now sil5.EndsAt = now.Add(5 * time.Minute) id5, err := s.Set(sil5) require.NoError(t, err) require.NotEqual(t, id2, id4) want = state{ id1: want[id1], id2: want[id2], id4: want[id4], id5: &pb.MeshSilence{ Silence: &pb.Silence{ Id: id5, Matchers: []*pb.Matcher{{Name: "a", Pattern: "b"}}, StartsAt: now5, EndsAt: now5.Add(5 * time.Minute), UpdatedAt: now5, }, ExpiresAt: now5.Add(5*time.Minute + s.retention), }, } require.Equal(t, want, s.st, "unexpected state after silence creation") } func TestSilencesSetFail(t *testing.T) { s, err := New(Options{}) require.NoError(t, err) now := utcNow() s.now = func() time.Time { return now } cases := []struct { s *pb.Silence err string }{ { s: &pb.Silence{Id: "some_id"}, err: ErrNotFound.Error(), }, { s: &pb.Silence{}, // Silence without matcher. err: "silence invalid", }, } for _, c := range cases { _, err := s.Set(c.s) if err == nil { if c.err != "" { t.Errorf("expected error containing %q but got none", c.err) } continue } if err != nil && c.err == "" { t.Errorf("unexpected error %q", err) continue } if !strings.Contains(err.Error(), c.err) { t.Errorf("expected error to contain %q but got %q", c.err, err) } } } func TestQState(t *testing.T) { now := utcNow() cases := []struct { sil *pb.Silence states []types.SilenceState keep bool }{ { sil: &pb.Silence{ StartsAt: now.Add(time.Minute), EndsAt: now.Add(time.Hour), }, states: []types.SilenceState{types.SilenceStateActive, types.SilenceStateExpired}, keep: false, }, { sil: &pb.Silence{ StartsAt: now.Add(time.Minute), EndsAt: now.Add(time.Hour), }, states: []types.SilenceState{types.SilenceStatePending}, keep: true, }, { sil: &pb.Silence{ StartsAt: now.Add(time.Minute), EndsAt: now.Add(time.Hour), }, states: []types.SilenceState{types.SilenceStateExpired, types.SilenceStatePending}, keep: true, }, } for i, c := range cases { q := &query{} QState(c.states...)(q) f := q.filters[0] keep, err := f(c.sil, nil, now) require.NoError(t, err) require.Equal(t, c.keep, keep, "unexpected filter result for case %d", i) } } func TestQMatches(t *testing.T) { qp := QMatches(model.LabelSet{ "job": "test", "instance": "web-1", "path": "/user/profile", "method": "GET", }) q := &query{} qp(q) f := q.filters[0] cases := []struct { sil *pb.Silence drop bool }{ { sil: &pb.Silence{ Matchers: []*pb.Matcher{ {Name: "job", Pattern: "test", Type: pb.Matcher_EQUAL}, }, }, drop: true, }, { sil: &pb.Silence{ Matchers: []*pb.Matcher{ {Name: "job", Pattern: "test", Type: pb.Matcher_EQUAL}, {Name: "method", Pattern: "POST", Type: pb.Matcher_EQUAL}, }, }, drop: false, }, { sil: &pb.Silence{ Matchers: []*pb.Matcher{ {Name: "path", Pattern: "/user/.+", Type: pb.Matcher_REGEXP}, }, }, drop: true, }, { sil: &pb.Silence{ Matchers: []*pb.Matcher{ {Name: "path", Pattern: "/user/.+", Type: pb.Matcher_REGEXP}, {Name: "path", Pattern: "/nothing/.+", Type: pb.Matcher_REGEXP}, }, }, drop: false, }, } for _, c := range cases { drop, err := f(c.sil, &Silences{mc: matcherCache{}, st: state{}}, time.Time{}) require.NoError(t, err) require.Equal(t, c.drop, drop, "unexpected filter result") } } func TestSilencesQuery(t *testing.T) { s, err := New(Options{}) require.NoError(t, err) s.st = state{ "1": &pb.MeshSilence{Silence: &pb.Silence{Id: "1"}}, "2": &pb.MeshSilence{Silence: &pb.Silence{Id: "2"}}, "3": &pb.MeshSilence{Silence: &pb.Silence{Id: "3"}}, "4": &pb.MeshSilence{Silence: &pb.Silence{Id: "4"}}, "5": &pb.MeshSilence{Silence: &pb.Silence{Id: "5"}}, } cases := []struct { q *query exp []*pb.Silence }{ { // Default query of retrieving all silences. q: &query{}, exp: []*pb.Silence{ {Id: "1"}, {Id: "2"}, {Id: "3"}, {Id: "4"}, {Id: "5"}, }, }, { // Retrieve by IDs. q: &query{ ids: []string{"2", "5"}, }, exp: []*pb.Silence{ {Id: "2"}, {Id: "5"}, }, }, { // Retrieve all and filter q: &query{ filters: []silenceFilter{ func(sil *pb.Silence, _ *Silences, _ time.Time) (bool, error) { return sil.Id == "1" || sil.Id == "2", nil }, }, }, exp: []*pb.Silence{ {Id: "1"}, {Id: "2"}, }, }, { // Retrieve by IDs and filter q: &query{ ids: []string{"2", "5"}, filters: []silenceFilter{ func(sil *pb.Silence, _ *Silences, _ time.Time) (bool, error) { return sil.Id == "1" || sil.Id == "2", nil }, }, }, exp: []*pb.Silence{ {Id: "2"}, }, }, } for _, c := range cases { // Run default query of retrieving all silences. res, err := s.query(c.q, time.Time{}) require.NoError(t, err, "unexpected error on querying") // Currently there are no sorting guarantees in the querying API. sort.Sort(silencesByID(c.exp)) sort.Sort(silencesByID(res)) require.Equal(t, c.exp, res, "unexpected silences in result") } } type silencesByID []*pb.Silence func (s silencesByID) Len() int { return len(s) } func (s silencesByID) Swap(i, j int) { s[i], s[j] = s[j], s[i] } func (s silencesByID) Less(i, j int) bool { return s[i].Id < s[j].Id } func TestSilenceCanUpdate(t *testing.T) { now := utcNow() cases := []struct { a, b *pb.Silence ok bool }{ // Bad arguments. { a: &pb.Silence{}, b: &pb.Silence{ StartsAt: now, EndsAt: now.Add(-time.Minute), }, ok: false, }, // Expired silence. { a: &pb.Silence{ StartsAt: now.Add(-time.Hour), EndsAt: now.Add(-time.Second), }, b: &pb.Silence{ StartsAt: now, EndsAt: now, }, ok: false, }, // Pending silences. { a: &pb.Silence{ StartsAt: now.Add(time.Hour), EndsAt: now.Add(2 * time.Hour), UpdatedAt: now.Add(-time.Hour), }, b: &pb.Silence{ StartsAt: now.Add(-time.Minute), EndsAt: now.Add(time.Hour), }, ok: false, }, { a: &pb.Silence{ StartsAt: now.Add(time.Hour), EndsAt: now.Add(2 * time.Hour), UpdatedAt: now.Add(-time.Hour), }, b: &pb.Silence{ StartsAt: now.Add(time.Minute), EndsAt: now.Add(time.Minute), }, ok: true, }, { a: &pb.Silence{ StartsAt: now.Add(time.Hour), EndsAt: now.Add(2 * time.Hour), UpdatedAt: now.Add(-time.Hour), }, b: &pb.Silence{ StartsAt: now, // set to exactly start now. EndsAt: now.Add(2 * time.Hour), }, ok: true, }, // Active silences. { a: &pb.Silence{ StartsAt: now.Add(-time.Hour), EndsAt: now.Add(2 * time.Hour), UpdatedAt: now.Add(-time.Hour), }, b: &pb.Silence{ StartsAt: now.Add(-time.Minute), EndsAt: now.Add(2 * time.Hour), }, ok: false, }, { a: &pb.Silence{ StartsAt: now.Add(-time.Hour), EndsAt: now.Add(2 * time.Hour), UpdatedAt: now.Add(-time.Hour), }, b: &pb.Silence{ StartsAt: now.Add(-time.Hour), EndsAt: now.Add(-time.Second), }, ok: false, }, { a: &pb.Silence{ StartsAt: now.Add(-time.Hour), EndsAt: now.Add(2 * time.Hour), UpdatedAt: now.Add(-time.Hour), }, b: &pb.Silence{ StartsAt: now.Add(-time.Hour), EndsAt: now, }, ok: true, }, { a: &pb.Silence{ StartsAt: now.Add(-time.Hour), EndsAt: now.Add(2 * time.Hour), UpdatedAt: now.Add(-time.Hour), }, b: &pb.Silence{ StartsAt: now.Add(-time.Hour), EndsAt: now.Add(3 * time.Hour), }, ok: true, }, } for _, c := range cases { ok := canUpdate(c.a, c.b, now) if ok && !c.ok { t.Errorf("expected not-updateable but was: %v, %v", c.a, c.b) } if ok && !c.ok { t.Errorf("expected updateable but was not: %v, %v", c.a, c.b) } } } func TestSilenceExpire(t *testing.T) { s, err := New(Options{}) require.NoError(t, err) now := time.Now() s.now = func() time.Time { return now } m := &pb.Matcher{Type: pb.Matcher_EQUAL, Name: "a", Pattern: "b"} s.st = state{ "pending": &pb.MeshSilence{Silence: &pb.Silence{ Id: "pending", Matchers: []*pb.Matcher{m}, StartsAt: now.Add(time.Minute), EndsAt: now.Add(time.Hour), UpdatedAt: now.Add(-time.Hour), }}, "active": &pb.MeshSilence{Silence: &pb.Silence{ Id: "active", Matchers: []*pb.Matcher{m}, StartsAt: now.Add(-time.Minute), EndsAt: now.Add(time.Hour), UpdatedAt: now.Add(-time.Hour), }}, "expired": &pb.MeshSilence{Silence: &pb.Silence{ Id: "expired", Matchers: []*pb.Matcher{m}, StartsAt: now.Add(-time.Hour), EndsAt: now.Add(-time.Minute), UpdatedAt: now.Add(-time.Hour), }}, } count, err := s.CountState(types.SilenceStatePending) require.NoError(t, err) require.Equal(t, 1, count) require.NoError(t, s.expire("pending")) require.NoError(t, s.expire("active")) err = s.expire("expired") require.Error(t, err) require.Contains(t, err.Error(), "already expired") sil, err := s.QueryOne(QIDs("pending")) require.NoError(t, err) require.Equal(t, &pb.Silence{ Id: "pending", Matchers: []*pb.Matcher{m}, StartsAt: now, EndsAt: now, UpdatedAt: now, }, sil) count, err = s.CountState(types.SilenceStatePending) require.NoError(t, err) require.Equal(t, 0, count) // Expiring a pending Silence should make the API return the // SilenceStateExpired Silence state. silenceState := types.CalcSilenceState(sil.StartsAt, sil.EndsAt) require.Equal(t, silenceState, types.SilenceStateExpired) sil, err = s.QueryOne(QIDs("active")) require.NoError(t, err) require.Equal(t, &pb.Silence{ Id: "active", Matchers: []*pb.Matcher{m}, StartsAt: now.Add(-time.Minute), EndsAt: now, UpdatedAt: now, }, sil) sil, err = s.QueryOne(QIDs("expired")) require.NoError(t, err) require.Equal(t, &pb.Silence{ Id: "expired", Matchers: []*pb.Matcher{m}, StartsAt: now.Add(-time.Hour), EndsAt: now.Add(-time.Minute), UpdatedAt: now.Add(-time.Hour), }, sil) } func TestValidateMatcher(t *testing.T) { cases := []struct { m *pb.Matcher err string }{ { m: &pb.Matcher{ Name: "a", Pattern: "b", Type: pb.Matcher_EQUAL, }, err: "", }, { m: &pb.Matcher{ Name: "00", Pattern: "a", Type: pb.Matcher_EQUAL, }, err: "invalid label name", }, { m: &pb.Matcher{ Name: "a", Pattern: "((", Type: pb.Matcher_REGEXP, }, err: "invalid regular expression", }, { m: &pb.Matcher{ Name: "a", Pattern: "\xff", Type: pb.Matcher_EQUAL, }, err: "invalid label value", }, { m: &pb.Matcher{ Name: "a", Pattern: "b", Type: 333, }, err: "unknown matcher type", }, } for _, c := range cases { err := validateMatcher(c.m) if err == nil { if c.err != "" { t.Errorf("expected error containing %q but got none", c.err) } continue } if err != nil && c.err == "" { t.Errorf("unexpected error %q", err) continue } if !strings.Contains(err.Error(), c.err) { t.Errorf("expected error to contain %q but got %q", c.err, err) } } } func TestValidateSilence(t *testing.T) { var ( now = utcNow() zeroTimestamp = time.Time{} validTimestamp = now ) cases := []struct { s *pb.Silence err string }{ { s: &pb.Silence{ Id: "some_id", Matchers: []*pb.Matcher{ &pb.Matcher{Name: "a", Pattern: "b"}, }, StartsAt: validTimestamp, EndsAt: validTimestamp, UpdatedAt: validTimestamp, }, err: "", }, { s: &pb.Silence{ Id: "", Matchers: []*pb.Matcher{ &pb.Matcher{Name: "a", Pattern: "b"}, }, StartsAt: validTimestamp, EndsAt: validTimestamp, UpdatedAt: validTimestamp, }, err: "ID missing", }, { s: &pb.Silence{ Id: "some_id", Matchers: []*pb.Matcher{}, StartsAt: validTimestamp, EndsAt: validTimestamp, UpdatedAt: validTimestamp, }, err: "at least one matcher required", }, { s: &pb.Silence{ Id: "some_id", Matchers: []*pb.Matcher{ &pb.Matcher{Name: "a", Pattern: "b"}, &pb.Matcher{Name: "00", Pattern: "b"}, }, StartsAt: validTimestamp, EndsAt: validTimestamp, UpdatedAt: validTimestamp, }, err: "invalid label matcher", }, { s: &pb.Silence{ Id: "some_id", Matchers: []*pb.Matcher{ &pb.Matcher{Name: "a", Pattern: "b"}, }, StartsAt: now, EndsAt: now.Add(-time.Second), UpdatedAt: validTimestamp, }, err: "end time must not be before start time", }, { s: &pb.Silence{ Id: "some_id", Matchers: []*pb.Matcher{ &pb.Matcher{Name: "a", Pattern: "b"}, }, StartsAt: zeroTimestamp, EndsAt: validTimestamp, UpdatedAt: validTimestamp, }, err: "invalid zero start timestamp", }, { s: &pb.Silence{ Id: "some_id", Matchers: []*pb.Matcher{ &pb.Matcher{Name: "a", Pattern: "b"}, }, StartsAt: validTimestamp, EndsAt: zeroTimestamp, UpdatedAt: validTimestamp, }, err: "invalid zero end timestamp", }, { s: &pb.Silence{ Id: "some_id", Matchers: []*pb.Matcher{ &pb.Matcher{Name: "a", Pattern: "b"}, }, StartsAt: validTimestamp, EndsAt: validTimestamp, UpdatedAt: zeroTimestamp, }, err: "invalid zero update timestamp", }, } for _, c := range cases { err := validateSilence(c.s) if err == nil { if c.err != "" { t.Errorf("expected error containing %q but got none", c.err) } continue } if err != nil && c.err == "" { t.Errorf("unexpected error %q", err) continue } if !strings.Contains(err.Error(), c.err) { t.Errorf("expected error to contain %q but got %q", c.err, err) } } } func TestStateMerge(t *testing.T) { now := utcNow() // We only care about key names and timestamps for the // merging logic. newSilence := func(id string, ts time.Time) *pb.MeshSilence { return &pb.MeshSilence{ Silence: &pb.Silence{Id: id, UpdatedAt: ts}, } } cases := []struct { a, b state final state }{ { a: state{ "a1": newSilence("a1", now), "a2": newSilence("a2", now), "a3": newSilence("a3", now), }, b: state{ "b1": newSilence("b1", now), // new key, should be added "a2": newSilence("a2", now.Add(-time.Minute)), // older timestamp, should be dropped "a3": newSilence("a3", now.Add(time.Minute)), // newer timestamp, should overwrite }, final: state{ "a1": newSilence("a1", now), "a2": newSilence("a2", now), "a3": newSilence("a3", now.Add(time.Minute)), "b1": newSilence("b1", now), }, }, } for _, c := range cases { for _, e := range c.b { c.a.merge(e) } require.Equal(t, c.final, c.a, "Merge result should match expectation") } } func TestStateCoding(t *testing.T) { // Check whether encoding and decoding the data is symmetric. now := utcNow() cases := []struct { entries []*pb.MeshSilence }{ { entries: []*pb.MeshSilence{ { Silence: &pb.Silence{ Id: "3be80475-e219-4ee7-b6fc-4b65114e362f", Matchers: []*pb.Matcher{ {Name: "label1", Pattern: "val1", Type: pb.Matcher_EQUAL}, {Name: "label2", Pattern: "val.+", Type: pb.Matcher_REGEXP}, }, StartsAt: now, EndsAt: now, UpdatedAt: now, }, ExpiresAt: now, }, { Silence: &pb.Silence{ Id: "4b1e760d-182c-4980-b873-c1a6827c9817", Matchers: []*pb.Matcher{ {Name: "label1", Pattern: "val1", Type: pb.Matcher_EQUAL}, }, StartsAt: now.Add(time.Hour), EndsAt: now.Add(2 * time.Hour), UpdatedAt: now, }, ExpiresAt: now.Add(24 * time.Hour), }, }, }, } for _, c := range cases { // Create gossip data from input. in := state{} for _, e := range c.entries { in[e.Silence.Id] = e } msg, err := in.MarshalBinary() require.NoError(t, err) out, err := decodeState(bytes.NewReader(msg)) require.NoError(t, err, "decoding message failed") require.Equal(t, in, out, "decoded data doesn't match encoded data") } } func TestStateDecodingError(t *testing.T) { // Check whether decoding copes with erroneous data. s := state{"": &pb.MeshSilence{}} msg, err := s.MarshalBinary() require.NoError(t, err) _, err = decodeState(bytes.NewReader(msg)) require.Equal(t, ErrInvalidState, err) } prometheus-alertmanager-0.15.3+ds/silence/silencepb/000077500000000000000000000000001341674552200224345ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/silence/silencepb/silence.pb.go000066400000000000000000000743761341674552200250260ustar00rootroot00000000000000// Code generated by protoc-gen-gogo. DO NOT EDIT. // source: silence.proto /* Package silencepb is a generated protocol buffer package. It is generated from these files: silence.proto It has these top-level messages: Matcher Comment Silence MeshSilence */ package silencepb import proto "github.com/gogo/protobuf/proto" import fmt "fmt" import math "math" import _ "github.com/gogo/protobuf/gogoproto" import time "time" import types "github.com/gogo/protobuf/types" import io "io" // Reference imports to suppress errors if they are not otherwise used. var _ = proto.Marshal var _ = fmt.Errorf var _ = math.Inf var _ = time.Kitchen // This is a compile-time assertion to ensure that this generated file // is compatible with the proto package it is being compiled against. // A compilation error at this line likely means your copy of the // proto package needs to be updated. const _ = proto.GoGoProtoPackageIsVersion2 // please upgrade the proto package // Type specifies how the given name and pattern are matched // against a label set. type Matcher_Type int32 const ( Matcher_EQUAL Matcher_Type = 0 Matcher_REGEXP Matcher_Type = 1 ) var Matcher_Type_name = map[int32]string{ 0: "EQUAL", 1: "REGEXP", } var Matcher_Type_value = map[string]int32{ "EQUAL": 0, "REGEXP": 1, } func (x Matcher_Type) String() string { return proto.EnumName(Matcher_Type_name, int32(x)) } func (Matcher_Type) EnumDescriptor() ([]byte, []int) { return fileDescriptorSilence, []int{0, 0} } // Matcher specifies a rule, which can match or set of labels or not. type Matcher struct { Type Matcher_Type `protobuf:"varint,1,opt,name=type,proto3,enum=silencepb.Matcher_Type" json:"type,omitempty"` // The label name in a label set to against which the matcher // checks the pattern. Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` // The pattern being checked according to the matcher's type. Pattern string `protobuf:"bytes,3,opt,name=pattern,proto3" json:"pattern,omitempty"` } func (m *Matcher) Reset() { *m = Matcher{} } func (m *Matcher) String() string { return proto.CompactTextString(m) } func (*Matcher) ProtoMessage() {} func (*Matcher) Descriptor() ([]byte, []int) { return fileDescriptorSilence, []int{0} } // DEPRECATED: A comment can be attached to a silence. type Comment struct { Author string `protobuf:"bytes,1,opt,name=author,proto3" json:"author,omitempty"` Comment string `protobuf:"bytes,2,opt,name=comment,proto3" json:"comment,omitempty"` Timestamp time.Time `protobuf:"bytes,3,opt,name=timestamp,stdtime" json:"timestamp"` } func (m *Comment) Reset() { *m = Comment{} } func (m *Comment) String() string { return proto.CompactTextString(m) } func (*Comment) ProtoMessage() {} func (*Comment) Descriptor() ([]byte, []int) { return fileDescriptorSilence, []int{1} } // Silence specifies an object that ignores alerts based // on a set of matchers during a given time frame. type Silence struct { // A globally unique identifier. Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // A set of matchers all of which have to be true for a silence // to affect a given label set. Matchers []*Matcher `protobuf:"bytes,2,rep,name=matchers" json:"matchers,omitempty"` // The time range during which the silence is active. StartsAt time.Time `protobuf:"bytes,3,opt,name=starts_at,json=startsAt,stdtime" json:"starts_at"` EndsAt time.Time `protobuf:"bytes,4,opt,name=ends_at,json=endsAt,stdtime" json:"ends_at"` // The last motification made to the silence. UpdatedAt time.Time `protobuf:"bytes,5,opt,name=updated_at,json=updatedAt,stdtime" json:"updated_at"` // DEPRECATED: A set of comments made on the silence. Comments []*Comment `protobuf:"bytes,7,rep,name=comments" json:"comments,omitempty"` // Comment for the silence. CreatedBy string `protobuf:"bytes,8,opt,name=created_by,json=createdBy,proto3" json:"created_by,omitempty"` Comment string `protobuf:"bytes,9,opt,name=comment,proto3" json:"comment,omitempty"` } func (m *Silence) Reset() { *m = Silence{} } func (m *Silence) String() string { return proto.CompactTextString(m) } func (*Silence) ProtoMessage() {} func (*Silence) Descriptor() ([]byte, []int) { return fileDescriptorSilence, []int{2} } // MeshSilence wraps a regular silence with an expiration timestamp // after which the silence may be garbage collected. type MeshSilence struct { Silence *Silence `protobuf:"bytes,1,opt,name=silence" json:"silence,omitempty"` ExpiresAt time.Time `protobuf:"bytes,2,opt,name=expires_at,json=expiresAt,stdtime" json:"expires_at"` } func (m *MeshSilence) Reset() { *m = MeshSilence{} } func (m *MeshSilence) String() string { return proto.CompactTextString(m) } func (*MeshSilence) ProtoMessage() {} func (*MeshSilence) Descriptor() ([]byte, []int) { return fileDescriptorSilence, []int{3} } func init() { proto.RegisterType((*Matcher)(nil), "silencepb.Matcher") proto.RegisterType((*Comment)(nil), "silencepb.Comment") proto.RegisterType((*Silence)(nil), "silencepb.Silence") proto.RegisterType((*MeshSilence)(nil), "silencepb.MeshSilence") proto.RegisterEnum("silencepb.Matcher_Type", Matcher_Type_name, Matcher_Type_value) } func (m *Matcher) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalTo(dAtA) if err != nil { return nil, err } return dAtA[:n], nil } func (m *Matcher) MarshalTo(dAtA []byte) (int, error) { var i int _ = i var l int _ = l if m.Type != 0 { dAtA[i] = 0x8 i++ i = encodeVarintSilence(dAtA, i, uint64(m.Type)) } if len(m.Name) > 0 { dAtA[i] = 0x12 i++ i = encodeVarintSilence(dAtA, i, uint64(len(m.Name))) i += copy(dAtA[i:], m.Name) } if len(m.Pattern) > 0 { dAtA[i] = 0x1a i++ i = encodeVarintSilence(dAtA, i, uint64(len(m.Pattern))) i += copy(dAtA[i:], m.Pattern) } return i, nil } func (m *Comment) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalTo(dAtA) if err != nil { return nil, err } return dAtA[:n], nil } func (m *Comment) MarshalTo(dAtA []byte) (int, error) { var i int _ = i var l int _ = l if len(m.Author) > 0 { dAtA[i] = 0xa i++ i = encodeVarintSilence(dAtA, i, uint64(len(m.Author))) i += copy(dAtA[i:], m.Author) } if len(m.Comment) > 0 { dAtA[i] = 0x12 i++ i = encodeVarintSilence(dAtA, i, uint64(len(m.Comment))) i += copy(dAtA[i:], m.Comment) } dAtA[i] = 0x1a i++ i = encodeVarintSilence(dAtA, i, uint64(types.SizeOfStdTime(m.Timestamp))) n1, err := types.StdTimeMarshalTo(m.Timestamp, dAtA[i:]) if err != nil { return 0, err } i += n1 return i, nil } func (m *Silence) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalTo(dAtA) if err != nil { return nil, err } return dAtA[:n], nil } func (m *Silence) MarshalTo(dAtA []byte) (int, error) { var i int _ = i var l int _ = l if len(m.Id) > 0 { dAtA[i] = 0xa i++ i = encodeVarintSilence(dAtA, i, uint64(len(m.Id))) i += copy(dAtA[i:], m.Id) } if len(m.Matchers) > 0 { for _, msg := range m.Matchers { dAtA[i] = 0x12 i++ i = encodeVarintSilence(dAtA, i, uint64(msg.Size())) n, err := msg.MarshalTo(dAtA[i:]) if err != nil { return 0, err } i += n } } dAtA[i] = 0x1a i++ i = encodeVarintSilence(dAtA, i, uint64(types.SizeOfStdTime(m.StartsAt))) n2, err := types.StdTimeMarshalTo(m.StartsAt, dAtA[i:]) if err != nil { return 0, err } i += n2 dAtA[i] = 0x22 i++ i = encodeVarintSilence(dAtA, i, uint64(types.SizeOfStdTime(m.EndsAt))) n3, err := types.StdTimeMarshalTo(m.EndsAt, dAtA[i:]) if err != nil { return 0, err } i += n3 dAtA[i] = 0x2a i++ i = encodeVarintSilence(dAtA, i, uint64(types.SizeOfStdTime(m.UpdatedAt))) n4, err := types.StdTimeMarshalTo(m.UpdatedAt, dAtA[i:]) if err != nil { return 0, err } i += n4 if len(m.Comments) > 0 { for _, msg := range m.Comments { dAtA[i] = 0x3a i++ i = encodeVarintSilence(dAtA, i, uint64(msg.Size())) n, err := msg.MarshalTo(dAtA[i:]) if err != nil { return 0, err } i += n } } if len(m.CreatedBy) > 0 { dAtA[i] = 0x42 i++ i = encodeVarintSilence(dAtA, i, uint64(len(m.CreatedBy))) i += copy(dAtA[i:], m.CreatedBy) } if len(m.Comment) > 0 { dAtA[i] = 0x4a i++ i = encodeVarintSilence(dAtA, i, uint64(len(m.Comment))) i += copy(dAtA[i:], m.Comment) } return i, nil } func (m *MeshSilence) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalTo(dAtA) if err != nil { return nil, err } return dAtA[:n], nil } func (m *MeshSilence) MarshalTo(dAtA []byte) (int, error) { var i int _ = i var l int _ = l if m.Silence != nil { dAtA[i] = 0xa i++ i = encodeVarintSilence(dAtA, i, uint64(m.Silence.Size())) n5, err := m.Silence.MarshalTo(dAtA[i:]) if err != nil { return 0, err } i += n5 } dAtA[i] = 0x12 i++ i = encodeVarintSilence(dAtA, i, uint64(types.SizeOfStdTime(m.ExpiresAt))) n6, err := types.StdTimeMarshalTo(m.ExpiresAt, dAtA[i:]) if err != nil { return 0, err } i += n6 return i, nil } func encodeVarintSilence(dAtA []byte, offset int, v uint64) int { for v >= 1<<7 { dAtA[offset] = uint8(v&0x7f | 0x80) v >>= 7 offset++ } dAtA[offset] = uint8(v) return offset + 1 } func (m *Matcher) Size() (n int) { var l int _ = l if m.Type != 0 { n += 1 + sovSilence(uint64(m.Type)) } l = len(m.Name) if l > 0 { n += 1 + l + sovSilence(uint64(l)) } l = len(m.Pattern) if l > 0 { n += 1 + l + sovSilence(uint64(l)) } return n } func (m *Comment) Size() (n int) { var l int _ = l l = len(m.Author) if l > 0 { n += 1 + l + sovSilence(uint64(l)) } l = len(m.Comment) if l > 0 { n += 1 + l + sovSilence(uint64(l)) } l = types.SizeOfStdTime(m.Timestamp) n += 1 + l + sovSilence(uint64(l)) return n } func (m *Silence) Size() (n int) { var l int _ = l l = len(m.Id) if l > 0 { n += 1 + l + sovSilence(uint64(l)) } if len(m.Matchers) > 0 { for _, e := range m.Matchers { l = e.Size() n += 1 + l + sovSilence(uint64(l)) } } l = types.SizeOfStdTime(m.StartsAt) n += 1 + l + sovSilence(uint64(l)) l = types.SizeOfStdTime(m.EndsAt) n += 1 + l + sovSilence(uint64(l)) l = types.SizeOfStdTime(m.UpdatedAt) n += 1 + l + sovSilence(uint64(l)) if len(m.Comments) > 0 { for _, e := range m.Comments { l = e.Size() n += 1 + l + sovSilence(uint64(l)) } } l = len(m.CreatedBy) if l > 0 { n += 1 + l + sovSilence(uint64(l)) } l = len(m.Comment) if l > 0 { n += 1 + l + sovSilence(uint64(l)) } return n } func (m *MeshSilence) Size() (n int) { var l int _ = l if m.Silence != nil { l = m.Silence.Size() n += 1 + l + sovSilence(uint64(l)) } l = types.SizeOfStdTime(m.ExpiresAt) n += 1 + l + sovSilence(uint64(l)) return n } func sovSilence(x uint64) (n int) { for { n++ x >>= 7 if x == 0 { break } } return n } func sozSilence(x uint64) (n int) { return sovSilence(uint64((x << 1) ^ uint64((int64(x) >> 63)))) } func (m *Matcher) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowSilence } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: Matcher: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: Matcher: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 0 { return fmt.Errorf("proto: wrong wireType = %d for field Type", wireType) } m.Type = 0 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowSilence } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ m.Type |= (Matcher_Type(b) & 0x7F) << shift if b < 0x80 { break } } case 2: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowSilence } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ stringLen |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } intStringLen := int(stringLen) if intStringLen < 0 { return ErrInvalidLengthSilence } postIndex := iNdEx + intStringLen if postIndex > l { return io.ErrUnexpectedEOF } m.Name = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex case 3: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Pattern", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowSilence } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ stringLen |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } intStringLen := int(stringLen) if intStringLen < 0 { return ErrInvalidLengthSilence } postIndex := iNdEx + intStringLen if postIndex > l { return io.ErrUnexpectedEOF } m.Pattern = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipSilence(dAtA[iNdEx:]) if err != nil { return err } if skippy < 0 { return ErrInvalidLengthSilence } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *Comment) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowSilence } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: Comment: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: Comment: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Author", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowSilence } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ stringLen |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } intStringLen := int(stringLen) if intStringLen < 0 { return ErrInvalidLengthSilence } postIndex := iNdEx + intStringLen if postIndex > l { return io.ErrUnexpectedEOF } m.Author = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex case 2: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Comment", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowSilence } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ stringLen |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } intStringLen := int(stringLen) if intStringLen < 0 { return ErrInvalidLengthSilence } postIndex := iNdEx + intStringLen if postIndex > l { return io.ErrUnexpectedEOF } m.Comment = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex case 3: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Timestamp", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowSilence } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= (int(b) & 0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthSilence } postIndex := iNdEx + msglen if postIndex > l { return io.ErrUnexpectedEOF } if err := types.StdTimeUnmarshal(&m.Timestamp, dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipSilence(dAtA[iNdEx:]) if err != nil { return err } if skippy < 0 { return ErrInvalidLengthSilence } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *Silence) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowSilence } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: Silence: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: Silence: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Id", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowSilence } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ stringLen |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } intStringLen := int(stringLen) if intStringLen < 0 { return ErrInvalidLengthSilence } postIndex := iNdEx + intStringLen if postIndex > l { return io.ErrUnexpectedEOF } m.Id = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex case 2: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Matchers", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowSilence } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= (int(b) & 0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthSilence } postIndex := iNdEx + msglen if postIndex > l { return io.ErrUnexpectedEOF } m.Matchers = append(m.Matchers, &Matcher{}) if err := m.Matchers[len(m.Matchers)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex case 3: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field StartsAt", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowSilence } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= (int(b) & 0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthSilence } postIndex := iNdEx + msglen if postIndex > l { return io.ErrUnexpectedEOF } if err := types.StdTimeUnmarshal(&m.StartsAt, dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex case 4: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field EndsAt", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowSilence } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= (int(b) & 0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthSilence } postIndex := iNdEx + msglen if postIndex > l { return io.ErrUnexpectedEOF } if err := types.StdTimeUnmarshal(&m.EndsAt, dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex case 5: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field UpdatedAt", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowSilence } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= (int(b) & 0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthSilence } postIndex := iNdEx + msglen if postIndex > l { return io.ErrUnexpectedEOF } if err := types.StdTimeUnmarshal(&m.UpdatedAt, dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex case 7: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Comments", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowSilence } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= (int(b) & 0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthSilence } postIndex := iNdEx + msglen if postIndex > l { return io.ErrUnexpectedEOF } m.Comments = append(m.Comments, &Comment{}) if err := m.Comments[len(m.Comments)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex case 8: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field CreatedBy", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowSilence } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ stringLen |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } intStringLen := int(stringLen) if intStringLen < 0 { return ErrInvalidLengthSilence } postIndex := iNdEx + intStringLen if postIndex > l { return io.ErrUnexpectedEOF } m.CreatedBy = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex case 9: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Comment", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowSilence } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ stringLen |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } intStringLen := int(stringLen) if intStringLen < 0 { return ErrInvalidLengthSilence } postIndex := iNdEx + intStringLen if postIndex > l { return io.ErrUnexpectedEOF } m.Comment = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipSilence(dAtA[iNdEx:]) if err != nil { return err } if skippy < 0 { return ErrInvalidLengthSilence } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *MeshSilence) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowSilence } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: MeshSilence: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: MeshSilence: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Silence", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowSilence } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= (int(b) & 0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthSilence } postIndex := iNdEx + msglen if postIndex > l { return io.ErrUnexpectedEOF } if m.Silence == nil { m.Silence = &Silence{} } if err := m.Silence.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex case 2: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field ExpiresAt", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowSilence } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= (int(b) & 0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthSilence } postIndex := iNdEx + msglen if postIndex > l { return io.ErrUnexpectedEOF } if err := types.StdTimeUnmarshal(&m.ExpiresAt, dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipSilence(dAtA[iNdEx:]) if err != nil { return err } if skippy < 0 { return ErrInvalidLengthSilence } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func skipSilence(dAtA []byte) (n int, err error) { l := len(dAtA) iNdEx := 0 for iNdEx < l { var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return 0, ErrIntOverflowSilence } if iNdEx >= l { return 0, io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } wireType := int(wire & 0x7) switch wireType { case 0: for shift := uint(0); ; shift += 7 { if shift >= 64 { return 0, ErrIntOverflowSilence } if iNdEx >= l { return 0, io.ErrUnexpectedEOF } iNdEx++ if dAtA[iNdEx-1] < 0x80 { break } } return iNdEx, nil case 1: iNdEx += 8 return iNdEx, nil case 2: var length int for shift := uint(0); ; shift += 7 { if shift >= 64 { return 0, ErrIntOverflowSilence } if iNdEx >= l { return 0, io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ length |= (int(b) & 0x7F) << shift if b < 0x80 { break } } iNdEx += length if length < 0 { return 0, ErrInvalidLengthSilence } return iNdEx, nil case 3: for { var innerWire uint64 var start int = iNdEx for shift := uint(0); ; shift += 7 { if shift >= 64 { return 0, ErrIntOverflowSilence } if iNdEx >= l { return 0, io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ innerWire |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } innerWireType := int(innerWire & 0x7) if innerWireType == 4 { break } next, err := skipSilence(dAtA[start:]) if err != nil { return 0, err } iNdEx = start + next } return iNdEx, nil case 4: return iNdEx, nil case 5: iNdEx += 4 return iNdEx, nil default: return 0, fmt.Errorf("proto: illegal wireType %d", wireType) } } panic("unreachable") } var ( ErrInvalidLengthSilence = fmt.Errorf("proto: negative length found during unmarshaling") ErrIntOverflowSilence = fmt.Errorf("proto: integer overflow") ) func init() { proto.RegisterFile("silence.proto", fileDescriptorSilence) } var fileDescriptorSilence = []byte{ // 444 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x51, 0x4d, 0x6b, 0xdb, 0x40, 0x10, 0xf5, 0x2a, 0x8e, 0x65, 0x8d, 0x69, 0x30, 0x43, 0x69, 0x85, 0x21, 0xb6, 0xd1, 0xc9, 0xd0, 0x22, 0x83, 0x7b, 0xee, 0x41, 0x0e, 0xa6, 0x97, 0x06, 0x5a, 0x35, 0x85, 0xde, 0xca, 0xda, 0x9a, 0xda, 0x82, 0x48, 0xbb, 0x48, 0x63, 0xa8, 0x4f, 0x2d, 0xf4, 0x0f, 0xf4, 0x67, 0xf9, 0xd8, 0x5f, 0xd0, 0x0f, 0xff, 0x8b, 0xde, 0x8a, 0x56, 0x2b, 0x37, 0x21, 0x27, 0xdf, 0x66, 0x66, 0xdf, 0x9b, 0xb7, 0xef, 0x0d, 0x3c, 0x2a, 0xd3, 0x5b, 0xca, 0x57, 0x14, 0xea, 0x42, 0xb1, 0x42, 0xcf, 0xb6, 0x7a, 0x39, 0x18, 0xad, 0x95, 0x5a, 0xdf, 0xd2, 0xd4, 0x3c, 0x2c, 0xb7, 0x9f, 0xa6, 0x9c, 0x66, 0x54, 0xb2, 0xcc, 0x74, 0x8d, 0x1d, 0x3c, 0x5e, 0xab, 0xb5, 0x32, 0xe5, 0xb4, 0xaa, 0xea, 0x69, 0xf0, 0x4d, 0x80, 0x7b, 0x2d, 0x79, 0xb5, 0xa1, 0x02, 0x9f, 0x41, 0x9b, 0x77, 0x9a, 0x7c, 0x31, 0x16, 0x93, 0x8b, 0xd9, 0xd3, 0xf0, 0xb8, 0x3c, 0xb4, 0x88, 0xf0, 0x66, 0xa7, 0x29, 0x36, 0x20, 0x44, 0x68, 0xe7, 0x32, 0x23, 0xdf, 0x19, 0x8b, 0x89, 0x17, 0x9b, 0x1a, 0x7d, 0x70, 0xb5, 0x64, 0xa6, 0x22, 0xf7, 0xcf, 0xcc, 0xb8, 0x69, 0x83, 0x4b, 0x68, 0x57, 0x5c, 0xf4, 0xe0, 0x7c, 0xf1, 0xf6, 0x7d, 0xf4, 0xba, 0xdf, 0x42, 0x80, 0x4e, 0xbc, 0x78, 0xb5, 0xf8, 0xf0, 0xa6, 0x2f, 0x82, 0x2f, 0xe0, 0x5e, 0xa9, 0x2c, 0xa3, 0x9c, 0xf1, 0x09, 0x74, 0xe4, 0x96, 0x37, 0xaa, 0x30, 0xdf, 0xf0, 0x62, 0xdb, 0x55, 0xbb, 0x57, 0x35, 0xc4, 0x4a, 0x36, 0x2d, 0xce, 0xc1, 0x3b, 0x7a, 0x35, 0xba, 0xbd, 0xd9, 0x20, 0xac, 0xd3, 0x08, 0x9b, 0x34, 0xc2, 0x9b, 0x06, 0x31, 0xef, 0xee, 0x7f, 0x8e, 0x5a, 0xdf, 0x7f, 0x8d, 0x44, 0xfc, 0x9f, 0x16, 0xfc, 0x75, 0xc0, 0x7d, 0x57, 0xdb, 0xc5, 0x0b, 0x70, 0xd2, 0xc4, 0xaa, 0x3b, 0x69, 0x82, 0x21, 0x74, 0xb3, 0xda, 0x7f, 0xe9, 0x3b, 0xe3, 0xb3, 0x49, 0x6f, 0x86, 0x0f, 0xa3, 0x89, 0x8f, 0x18, 0x8c, 0xc0, 0x2b, 0x59, 0x16, 0x5c, 0x7e, 0x94, 0x7c, 0xd2, 0x7f, 0xba, 0x35, 0x2d, 0x62, 0x7c, 0x09, 0x2e, 0xe5, 0x89, 0x59, 0xd0, 0x3e, 0x61, 0x41, 0xa7, 0x22, 0x45, 0x8c, 0x57, 0x00, 0x5b, 0x9d, 0x48, 0xa6, 0xa4, 0xda, 0x70, 0x7e, 0x4a, 0x24, 0x96, 0x17, 0x71, 0x65, 0xdb, 0x26, 0x5c, 0xfa, 0xee, 0x03, 0xdb, 0xf6, 0x5c, 0xf1, 0x11, 0x83, 0x97, 0x00, 0xab, 0x82, 0x8c, 0xe8, 0x72, 0xe7, 0x77, 0x4d, 0x7c, 0x9e, 0x9d, 0xcc, 0x77, 0x77, 0xef, 0xe7, 0xdd, 0xbb, 0x5f, 0xf0, 0x55, 0x40, 0xef, 0x9a, 0xca, 0x4d, 0x93, 0xff, 0x73, 0x70, 0xad, 0x8e, 0x39, 0xc2, 0x7d, 0x5d, 0x0b, 0x8a, 0x1b, 0x48, 0xe5, 0x95, 0x3e, 0xeb, 0xb4, 0x20, 0x93, 0x96, 0x73, 0x8a, 0x57, 0xcb, 0x8b, 0x78, 0xde, 0xdf, 0xff, 0x19, 0xb6, 0xf6, 0x87, 0xa1, 0xf8, 0x71, 0x18, 0x8a, 0xdf, 0x87, 0xa1, 0x58, 0x76, 0x0c, 0xf5, 0xc5, 0xbf, 0x00, 0x00, 0x00, 0xff, 0xff, 0xde, 0x36, 0xea, 0xdd, 0x71, 0x03, 0x00, 0x00, } prometheus-alertmanager-0.15.3+ds/silence/silencepb/silence.proto000066400000000000000000000040761341674552200251520ustar00rootroot00000000000000syntax = "proto3"; package silencepb; import "google/protobuf/timestamp.proto"; import "gogoproto/gogo.proto"; option (gogoproto.marshaler_all) = true; option (gogoproto.sizer_all) = true; option (gogoproto.unmarshaler_all) = true; option (gogoproto.goproto_getters_all) = false; // Matcher specifies a rule, which can match or set of labels or not. message Matcher { // Type specifies how the given name and pattern are matched // against a label set. enum Type { EQUAL = 0; REGEXP = 1; }; Type type = 1; // The label name in a label set to against which the matcher // checks the pattern. string name = 2; // The pattern being checked according to the matcher's type. string pattern = 3; } // DEPRECATED: A comment can be attached to a silence. message Comment { string author = 1; string comment = 2; google.protobuf.Timestamp timestamp = 3 [(gogoproto.stdtime) = true, (gogoproto.nullable) = false]; } // Silence specifies an object that ignores alerts based // on a set of matchers during a given time frame. message Silence { // A globally unique identifier. string id = 1; // A set of matchers all of which have to be true for a silence // to affect a given label set. repeated Matcher matchers = 2; // The time range during which the silence is active. google.protobuf.Timestamp starts_at = 3 [(gogoproto.stdtime) = true, (gogoproto.nullable) = false]; google.protobuf.Timestamp ends_at = 4 [(gogoproto.stdtime) = true, (gogoproto.nullable) = false]; // The last motification made to the silence. google.protobuf.Timestamp updated_at = 5 [(gogoproto.stdtime) = true, (gogoproto.nullable) = false]; // DEPRECATED: A set of comments made on the silence. repeated Comment comments = 7; // Comment for the silence. string created_by = 8; string comment = 9; } // MeshSilence wraps a regular silence with an expiration timestamp // after which the silence may be garbage collected. message MeshSilence { Silence silence = 1; google.protobuf.Timestamp expires_at = 2 [(gogoproto.stdtime) = true, (gogoproto.nullable) = false]; }prometheus-alertmanager-0.15.3+ds/template/000077500000000000000000000000001341674552200206615ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/template/default.tmpl000066400000000000000000000415721341674552200232140ustar00rootroot00000000000000{{ define "__alertmanager" }}AlertManager{{ end }} {{ define "__alertmanagerURL" }}{{ .ExternalURL }}/#/alerts?receiver={{ .Receiver }}{{ end }} {{ define "__subject" }}[{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ end }}] {{ .GroupLabels.SortedPairs.Values | join " " }} {{ if gt (len .CommonLabels) (len .GroupLabels) }}({{ with .CommonLabels.Remove .GroupLabels.Names }}{{ .Values | join " " }}{{ end }}){{ end }}{{ end }} {{ define "__description" }}{{ end }} {{ define "__text_alert_list" }}{{ range . }}Labels: {{ range .Labels.SortedPairs }} - {{ .Name }} = {{ .Value }} {{ end }}Annotations: {{ range .Annotations.SortedPairs }} - {{ .Name }} = {{ .Value }} {{ end }}Source: {{ .GeneratorURL }} {{ end }}{{ end }} {{ define "slack.default.title" }}{{ template "__subject" . }}{{ end }} {{ define "slack.default.username" }}{{ template "__alertmanager" . }}{{ end }} {{ define "slack.default.fallback" }}{{ template "slack.default.title" . }} | {{ template "slack.default.titlelink" . }}{{ end }} {{ define "slack.default.pretext" }}{{ end }} {{ define "slack.default.titlelink" }}{{ template "__alertmanagerURL" . }}{{ end }} {{ define "slack.default.iconemoji" }}{{ end }} {{ define "slack.default.iconurl" }}{{ end }} {{ define "slack.default.text" }}{{ end }} {{ define "slack.default.footer" }}{{ end }} {{ define "hipchat.default.from" }}{{ template "__alertmanager" . }}{{ end }} {{ define "hipchat.default.message" }}{{ template "__subject" . }}{{ end }} {{ define "pagerduty.default.description" }}{{ template "__subject" . }}{{ end }} {{ define "pagerduty.default.client" }}{{ template "__alertmanager" . }}{{ end }} {{ define "pagerduty.default.clientURL" }}{{ template "__alertmanagerURL" . }}{{ end }} {{ define "pagerduty.default.instances" }}{{ template "__text_alert_list" . }}{{ end }} {{ define "opsgenie.default.message" }}{{ template "__subject" . }}{{ end }} {{ define "opsgenie.default.description" }}{{ .CommonAnnotations.SortedPairs.Values | join " " }} {{ if gt (len .Alerts.Firing) 0 -}} Alerts Firing: {{ template "__text_alert_list" .Alerts.Firing }} {{- end }} {{ if gt (len .Alerts.Resolved) 0 -}} Alerts Resolved: {{ template "__text_alert_list" .Alerts.Resolved }} {{- end }} {{- end }} {{ define "opsgenie.default.source" }}{{ template "__alertmanagerURL" . }}{{ end }} {{ define "wechat.default.message" }}{{ template "__subject" . }} {{ .CommonAnnotations.SortedPairs.Values | join " " }} {{ if gt (len .Alerts.Firing) 0 -}} Alerts Firing: {{ template "__text_alert_list" .Alerts.Firing }} {{- end }} {{ if gt (len .Alerts.Resolved) 0 -}} Alerts Resolved: {{ template "__text_alert_list" .Alerts.Resolved }} {{- end }} AlertmanagerUrl: {{ template "__alertmanagerURL" . }} {{- end }} {{ define "wechat.default.api_secret" }}{{ end }} {{ define "wechat.default.to_user" }}{{ end }} {{ define "wechat.default.to_party" }}{{ end }} {{ define "wechat.default.to_tag" }}{{ end }} {{ define "wechat.default.agent_id" }}{{ end }} {{ define "victorops.default.state_message" }}{{ .CommonAnnotations.SortedPairs.Values | join " " }} {{ if gt (len .Alerts.Firing) 0 -}} Alerts Firing: {{ template "__text_alert_list" .Alerts.Firing }} {{- end }} {{ if gt (len .Alerts.Resolved) 0 -}} Alerts Resolved: {{ template "__text_alert_list" .Alerts.Resolved }} {{- end }} {{- end }} {{ define "victorops.default.entity_display_name" }}{{ template "__subject" . }}{{ end }} {{ define "victorops.default.monitoring_tool" }}{{ template "__alertmanager" . }}{{ end }} {{ define "email.default.subject" }}{{ template "__subject" . }}{{ end }} {{ define "email.default.html" }} {{ template "__subject" . }}
{{ .Alerts | len }} alert{{ if gt (len .Alerts) 1 }}s{{ end }} for {{ range .GroupLabels.SortedPairs }} {{ .Name }}={{ .Value }} {{ end }}
{{ if gt (len .Alerts.Firing) 0 }} {{ end }} {{ range .Alerts.Firing }} {{ end }} {{ if gt (len .Alerts.Resolved) 0 }} {{ if gt (len .Alerts.Firing) 0 }} {{ end }} {{ end }} {{ range .Alerts.Resolved }} {{ end }}
View in {{ template "__alertmanager" . }}
[{{ .Alerts.Firing | len }}] Firing
Labels
{{ range .Labels.SortedPairs }}{{ .Name }} = {{ .Value }}
{{ end }} {{ if gt (len .Annotations) 0 }}Annotations
{{ end }} {{ range .Annotations.SortedPairs }}{{ .Name }} = {{ .Value }}
{{ end }} Source



[{{ .Alerts.Resolved | len }}] Resolved
Labels
{{ range .Labels.SortedPairs }}{{ .Name }} = {{ .Value }}
{{ end }} {{ if gt (len .Annotations) 0 }}Annotations
{{ end }} {{ range .Annotations.SortedPairs }}{{ .Name }} = {{ .Value }}
{{ end }} Source
{{ end }} {{ define "pushover.default.title" }}{{ template "__subject" . }}{{ end }} {{ define "pushover.default.message" }}{{ .CommonAnnotations.SortedPairs.Values | join " " }} {{ if gt (len .Alerts.Firing) 0 }} Alerts Firing: {{ template "__text_alert_list" .Alerts.Firing }} {{ end }} {{ if gt (len .Alerts.Resolved) 0 }} Alerts Resolved: {{ template "__text_alert_list" .Alerts.Resolved }} {{ end }} {{ end }} {{ define "pushover.default.url" }}{{ template "__alertmanagerURL" . }}{{ end }} prometheus-alertmanager-0.15.3+ds/template/email.html000066400000000000000000000236051341674552200226440ustar00rootroot00000000000000 {{ template "__subject" . }}
{{ if gt (len .Alerts.Firing) 0 }}
{{ else }} {{ end }} {{ .Alerts | len }} alert{{ if gt (len .Alerts) 1 }}s{{ end }} for {{ range .GroupLabels.SortedPairs }} {{ .Name }}={{ .Value }} {{ end }}
{{ if gt (len .Alerts.Firing) 0 }} {{ end }} {{ range .Alerts.Firing }} {{ end }} {{ if gt (len .Alerts.Resolved) 0 }} {{ if gt (len .Alerts.Firing) 0 }} {{ end }} {{ end }} {{ range .Alerts.Resolved }} {{ end }}
View in {{ template "__alertmanager" . }}
[{{ .Alerts.Firing | len }}] Firing
Labels
{{ range .Labels.SortedPairs }}{{ .Name }} = {{ .Value }}
{{ end }} {{ if gt (len .Annotations) 0 }}Annotations
{{ end }} {{ range .Annotations.SortedPairs }}{{ .Name }} = {{ .Value }}
{{ end }} Source



[{{ .Alerts.Resolved | len }}] Resolved
Labels
{{ range .Labels.SortedPairs }}{{ .Name }} = {{ .Value }}
{{ end }} {{ if gt (len .Annotations) 0 }}Annotations
{{ end }} {{ range .Annotations.SortedPairs }}{{ .Name }} = {{ .Value }}
{{ end }} Source
prometheus-alertmanager-0.15.3+ds/template/template.go000066400000000000000000000203001341674552200230160ustar00rootroot00000000000000// Copyright 2015 Prometheus Team // 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 template import ( "bytes" "net/url" "path/filepath" "regexp" "sort" "strings" "time" tmplhtml "html/template" tmpltext "text/template" "github.com/prometheus/common/model" "github.com/prometheus/alertmanager/template/internal/deftmpl" "github.com/prometheus/alertmanager/types" ) // Template bundles a text and a html template instance. type Template struct { text *tmpltext.Template html *tmplhtml.Template ExternalURL *url.URL } // FromGlobs calls ParseGlob on all path globs provided and returns the // resulting Template. func FromGlobs(paths ...string) (*Template, error) { t := &Template{ text: tmpltext.New("").Option("missingkey=zero"), html: tmplhtml.New("").Option("missingkey=zero"), } var err error t.text = t.text.Funcs(tmpltext.FuncMap(DefaultFuncs)) t.html = t.html.Funcs(tmplhtml.FuncMap(DefaultFuncs)) b, err := deftmpl.Asset("template/default.tmpl") if err != nil { return nil, err } if t.text, err = t.text.Parse(string(b)); err != nil { return nil, err } if t.html, err = t.html.Parse(string(b)); err != nil { return nil, err } for _, tp := range paths { // ParseGlob in the template packages errors if not at least one file is // matched. We want to allow empty matches that may be populated later on. p, err := filepath.Glob(tp) if err != nil { return nil, err } if len(p) > 0 { if t.text, err = t.text.ParseGlob(tp); err != nil { return nil, err } if t.html, err = t.html.ParseGlob(tp); err != nil { return nil, err } } } return t, nil } // ExecuteTextString needs a meaningful doc comment (TODO(fabxc)). func (t *Template) ExecuteTextString(text string, data interface{}) (string, error) { if text == "" { return "", nil } tmpl, err := t.text.Clone() if err != nil { return "", err } tmpl, err = tmpl.New("").Option("missingkey=zero").Parse(text) if err != nil { return "", err } var buf bytes.Buffer err = tmpl.Execute(&buf, data) return buf.String(), err } // ExecuteHTMLString needs a meaningful doc comment (TODO(fabxc)). func (t *Template) ExecuteHTMLString(html string, data interface{}) (string, error) { if html == "" { return "", nil } tmpl, err := t.html.Clone() if err != nil { return "", err } tmpl, err = tmpl.New("").Option("missingkey=zero").Parse(html) if err != nil { return "", err } var buf bytes.Buffer err = tmpl.Execute(&buf, data) return buf.String(), err } type FuncMap map[string]interface{} var DefaultFuncs = FuncMap{ "toUpper": strings.ToUpper, "toLower": strings.ToLower, "title": strings.Title, // join is equal to strings.Join but inverts the argument order // for easier pipelining in templates. "join": func(sep string, s []string) string { return strings.Join(s, sep) }, "safeHtml": func(text string) tmplhtml.HTML { return tmplhtml.HTML(text) }, "reReplaceAll": func(pattern, repl, text string) string { re := regexp.MustCompile(pattern) return re.ReplaceAllString(text, repl) }, } // Pair is a key/value string pair. type Pair struct { Name, Value string } // Pairs is a list of key/value string pairs. type Pairs []Pair // Names returns a list of names of the pairs. func (ps Pairs) Names() []string { ns := make([]string, 0, len(ps)) for _, p := range ps { ns = append(ns, p.Name) } return ns } // Values returns a list of values of the pairs. func (ps Pairs) Values() []string { vs := make([]string, 0, len(ps)) for _, p := range ps { vs = append(vs, p.Value) } return vs } // KV is a set of key/value string pairs. type KV map[string]string // SortedPairs returns a sorted list of key/value pairs. func (kv KV) SortedPairs() Pairs { var ( pairs = make([]Pair, 0, len(kv)) keys = make([]string, 0, len(kv)) sortStart = 0 ) for k := range kv { if k == string(model.AlertNameLabel) { keys = append([]string{k}, keys...) sortStart = 1 } else { keys = append(keys, k) } } sort.Strings(keys[sortStart:]) for _, k := range keys { pairs = append(pairs, Pair{k, kv[k]}) } return pairs } // Remove returns a copy of the key/value set without the given keys. func (kv KV) Remove(keys []string) KV { keySet := make(map[string]struct{}, len(keys)) for _, k := range keys { keySet[k] = struct{}{} } res := KV{} for k, v := range kv { if _, ok := keySet[k]; !ok { res[k] = v } } return res } // Names returns the names of the label names in the LabelSet. func (kv KV) Names() []string { return kv.SortedPairs().Names() } // Values returns a list of the values in the LabelSet. func (kv KV) Values() []string { return kv.SortedPairs().Values() } // Data is the data passed to notification templates and webhook pushes. // // End-users should not be exposed to Go's type system, as this will confuse them and prevent // simple things like simple equality checks to fail. Map everything to float64/string. type Data struct { Receiver string `json:"receiver"` Status string `json:"status"` Alerts Alerts `json:"alerts"` GroupLabels KV `json:"groupLabels"` CommonLabels KV `json:"commonLabels"` CommonAnnotations KV `json:"commonAnnotations"` ExternalURL string `json:"externalURL"` } // Alert holds one alert for notification templates. type Alert struct { Status string `json:"status"` Labels KV `json:"labels"` Annotations KV `json:"annotations"` StartsAt time.Time `json:"startsAt"` EndsAt time.Time `json:"endsAt"` GeneratorURL string `json:"generatorURL"` } // Alerts is a list of Alert objects. type Alerts []Alert // Firing returns the subset of alerts that are firing. func (as Alerts) Firing() []Alert { res := []Alert{} for _, a := range as { if a.Status == string(model.AlertFiring) { res = append(res, a) } } return res } // Resolved returns the subset of alerts that are resolved. func (as Alerts) Resolved() []Alert { res := []Alert{} for _, a := range as { if a.Status == string(model.AlertResolved) { res = append(res, a) } } return res } // Data assembles data for template expansion. func (t *Template) Data(recv string, groupLabels model.LabelSet, alerts ...*types.Alert) *Data { data := &Data{ Receiver: regexp.QuoteMeta(strings.SplitN(recv, "/", 2)[0]), Status: string(types.Alerts(alerts...).Status()), Alerts: make(Alerts, 0, len(alerts)), GroupLabels: KV{}, CommonLabels: KV{}, CommonAnnotations: KV{}, ExternalURL: t.ExternalURL.String(), } // The call to types.Alert is necessary to correctly resolve the internal // representation to the user representation. for _, a := range types.Alerts(alerts...) { alert := Alert{ Status: string(a.Status()), Labels: make(KV, len(a.Labels)), Annotations: make(KV, len(a.Annotations)), StartsAt: a.StartsAt, EndsAt: a.EndsAt, GeneratorURL: a.GeneratorURL, } for k, v := range a.Labels { alert.Labels[string(k)] = string(v) } for k, v := range a.Annotations { alert.Annotations[string(k)] = string(v) } data.Alerts = append(data.Alerts, alert) } for k, v := range groupLabels { data.GroupLabels[string(k)] = string(v) } if len(alerts) >= 1 { var ( commonLabels = alerts[0].Labels.Clone() commonAnnotations = alerts[0].Annotations.Clone() ) for _, a := range alerts[1:] { for ln, lv := range commonLabels { if a.Labels[ln] != lv { delete(commonLabels, ln) } } for an, av := range commonAnnotations { if a.Annotations[an] != av { delete(commonAnnotations, an) } } } for k, v := range commonLabels { data.CommonLabels[string(k)] = string(v) } for k, v := range commonAnnotations { data.CommonAnnotations[string(k)] = string(v) } } return data } prometheus-alertmanager-0.15.3+ds/template/template_test.go000066400000000000000000000056361341674552200240740ustar00rootroot00000000000000// Copyright 2018 Prometheus Team // 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 template import ( "testing" "github.com/prometheus/common/model" "github.com/stretchr/testify/require" ) func TestPairNames(t *testing.T) { pairs := Pairs{ {"name1", "value1"}, {"name2", "value2"}, {"name3", "value3"}, } expected := []string{"name1", "name2", "name3"} require.EqualValues(t, expected, pairs.Names()) } func TestPairValues(t *testing.T) { pairs := Pairs{ {"name1", "value1"}, {"name2", "value2"}, {"name3", "value3"}, } expected := []string{"value1", "value2", "value3"} require.EqualValues(t, expected, pairs.Values()) } func TestKVSortedPairs(t *testing.T) { kv := KV{"d": "dVal", "b": "bVal", "c": "cVal"} expectedPairs := Pairs{ {"b", "bVal"}, {"c", "cVal"}, {"d", "dVal"}, } for i, p := range kv.SortedPairs() { require.EqualValues(t, p.Name, expectedPairs[i].Name) require.EqualValues(t, p.Value, expectedPairs[i].Value) } // validates alertname always comes first kv = KV{"d": "dVal", "b": "bVal", "c": "cVal", "alertname": "alert", "a": "aVal"} expectedPairs = Pairs{ {"alertname", "alert"}, {"a", "aVal"}, {"b", "bVal"}, {"c", "cVal"}, {"d", "dVal"}, } for i, p := range kv.SortedPairs() { require.EqualValues(t, p.Name, expectedPairs[i].Name) require.EqualValues(t, p.Value, expectedPairs[i].Value) } } func TestKVRemove(t *testing.T) { kv := KV{ "key1": "val1", "key2": "val2", "key3": "val3", "key4": "val4", } kv = kv.Remove([]string{"key2", "key4"}) expected := []string{"key1", "key3"} require.EqualValues(t, expected, kv.Names()) } func TestAlertsFiring(t *testing.T) { alerts := Alerts{ {Status: string(model.AlertFiring)}, {Status: string(model.AlertResolved)}, {Status: string(model.AlertFiring)}, {Status: string(model.AlertResolved)}, {Status: string(model.AlertResolved)}, } for _, alert := range alerts.Firing() { if alert.Status != string(model.AlertFiring) { t.Errorf("unexpected status %q", alert.Status) } } } func TestAlertsResolved(t *testing.T) { alerts := Alerts{ {Status: string(model.AlertFiring)}, {Status: string(model.AlertResolved)}, {Status: string(model.AlertFiring)}, {Status: string(model.AlertResolved)}, {Status: string(model.AlertResolved)}, } for _, alert := range alerts.Resolved() { if alert.Status != string(model.AlertResolved) { t.Errorf("unexpected status %q", alert.Status) } } } prometheus-alertmanager-0.15.3+ds/test/000077500000000000000000000000001341674552200200255ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/test/acceptance.go000066400000000000000000000236671341674552200224600ustar00rootroot00000000000000// Copyright 2015 Prometheus Team // 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 test import ( "bytes" "encoding/json" "fmt" "io/ioutil" "net" "net/http" "os" "os/exec" "path/filepath" "sync" "syscall" "testing" "time" "github.com/prometheus/client_golang/api" "github.com/prometheus/common/model" "golang.org/x/net/context" "github.com/prometheus/alertmanager/client" ) // AcceptanceTest provides declarative definition of given inputs and expected // output of an Alertmanager setup. type AcceptanceTest struct { *testing.T opts *AcceptanceOpts ams []*Alertmanager collectors []*Collector actions map[float64][]func() } // AcceptanceOpts defines configuration paramters for an acceptance test. type AcceptanceOpts struct { Tolerance time.Duration baseTime time.Time } func (opts *AcceptanceOpts) alertString(a *model.Alert) string { if a.EndsAt.IsZero() { return fmt.Sprintf("%s[%v:]", a, opts.relativeTime(a.StartsAt)) } return fmt.Sprintf("%s[%v:%v]", a, opts.relativeTime(a.StartsAt), opts.relativeTime(a.EndsAt)) } // expandTime returns the absolute time for the relative time // calculated from the test's base time. func (opts *AcceptanceOpts) expandTime(rel float64) time.Time { return opts.baseTime.Add(time.Duration(rel * float64(time.Second))) } // expandTime returns the relative time for the given time // calculated from the test's base time. func (opts *AcceptanceOpts) relativeTime(act time.Time) float64 { return float64(act.Sub(opts.baseTime)) / float64(time.Second) } // NewAcceptanceTest returns a new acceptance test with the base time // set to the current time. func NewAcceptanceTest(t *testing.T, opts *AcceptanceOpts) *AcceptanceTest { test := &AcceptanceTest{ T: t, opts: opts, actions: map[float64][]func(){}, } opts.baseTime = time.Now() return test } // freeAddress returns a new listen address not currently in use. func freeAddress() string { // Let the OS allocate a free address, close it and hope // it is still free when starting Alertmanager. l, err := net.Listen("tcp4", "localhost:0") if err != nil { panic(err) } defer l.Close() return l.Addr().String() } // Do sets the given function to be executed at the given time. func (t *AcceptanceTest) Do(at float64, f func()) { t.actions[at] = append(t.actions[at], f) } // Alertmanager returns a new structure that allows starting an instance // of Alertmanager on a random port. func (t *AcceptanceTest) Alertmanager(conf string) *Alertmanager { am := &Alertmanager{ t: t, opts: t.opts, } dir, err := ioutil.TempDir("", "am_test") if err != nil { t.Fatal(err) } am.dir = dir cf, err := os.Create(filepath.Join(dir, "config.yml")) if err != nil { t.Fatal(err) } am.confFile = cf am.UpdateConfig(conf) am.apiAddr = freeAddress() am.clusterAddr = freeAddress() t.Logf("AM on %s", am.apiAddr) c, err := api.NewClient(api.Config{ Address: fmt.Sprintf("http://%s", am.apiAddr), }) if err != nil { t.Fatal(err) } am.client = c t.ams = append(t.ams, am) return am } // Collector returns a new collector bound to the test instance. func (t *AcceptanceTest) Collector(name string) *Collector { co := &Collector{ t: t.T, name: name, opts: t.opts, collected: map[float64][]model.Alerts{}, expected: map[Interval][]model.Alerts{}, } t.collectors = append(t.collectors, co) return co } // Run starts all Alertmanagers and runs queries against them. It then checks // whether all expected notifications have arrived at the expected receiver. func (t *AcceptanceTest) Run() { errc := make(chan error) for _, am := range t.ams { am.errc = errc am.Start() defer func(am *Alertmanager) { am.Terminate() am.cleanup() t.Logf("stdout:\n%v", am.cmd.Stdout) t.Logf("stderr:\n%v", am.cmd.Stderr) }(am) } go t.runActions() var latest float64 for _, coll := range t.collectors { if l := coll.latest(); l > latest { latest = l } } deadline := t.opts.expandTime(latest) select { case <-time.After(deadline.Sub(time.Now())): // continue case err := <-errc: t.Error(err) } for _, coll := range t.collectors { report := coll.check() t.Log(report) } } // runActions performs the stored actions at the defined times. func (t *AcceptanceTest) runActions() { var wg sync.WaitGroup for at, fs := range t.actions { ts := t.opts.expandTime(at) wg.Add(len(fs)) for _, f := range fs { go func(f func()) { time.Sleep(ts.Sub(time.Now())) f() wg.Done() }(f) } } wg.Wait() } type buffer struct { b bytes.Buffer mtx sync.Mutex } func (b *buffer) Write(p []byte) (int, error) { b.mtx.Lock() defer b.mtx.Unlock() return b.b.Write(p) } func (b *buffer) String() string { b.mtx.Lock() defer b.mtx.Unlock() return b.b.String() } // Alertmanager encapsulates an Alertmanager process and allows // declaring alerts being pushed to it at fixed points in time. type Alertmanager struct { t *AcceptanceTest opts *AcceptanceOpts apiAddr string clusterAddr string client api.Client cmd *exec.Cmd confFile *os.File dir string errc chan<- error } // Start the alertmanager and wait until it is ready to receive. func (am *Alertmanager) Start() { cmd := exec.Command("../../alertmanager", "--config.file", am.confFile.Name(), "--log.level", "debug", "--web.listen-address", am.apiAddr, "--storage.path", am.dir, "--cluster.listen-address", am.clusterAddr, "--cluster.settle-timeout", "0s", ) if am.cmd == nil { var outb, errb buffer cmd.Stdout = &outb cmd.Stderr = &errb } else { cmd.Stdout = am.cmd.Stdout cmd.Stderr = am.cmd.Stderr } am.cmd = cmd if err := am.cmd.Start(); err != nil { am.t.Fatalf("Starting alertmanager failed: %s", err) } go func() { if err := am.cmd.Wait(); err != nil { am.errc <- err } }() time.Sleep(50 * time.Millisecond) for i := 0; i < 10; i++ { resp, err := http.Get(fmt.Sprintf("http://%s/status", am.apiAddr)) if err == nil { _, err := ioutil.ReadAll(resp.Body) if err != nil { am.t.Fatalf("Starting alertmanager failed: %s", err) } resp.Body.Close() return } time.Sleep(500 * time.Millisecond) } am.t.Fatalf("Starting alertmanager failed: timeout") } // Terminate kills the underlying Alertmanager process and remove intermediate // data. func (am *Alertmanager) Terminate() { if err := syscall.Kill(am.cmd.Process.Pid, syscall.SIGTERM); err != nil { am.t.Fatalf("Error sending SIGTERM to Alertmanager process: %v", err) } } // Reload sends the reloading signal to the Alertmanager process. func (am *Alertmanager) Reload() { if err := syscall.Kill(am.cmd.Process.Pid, syscall.SIGHUP); err != nil { am.t.Fatalf("Error sending SIGHUP to Alertmanager process: %v", err) } } func (am *Alertmanager) cleanup() { if err := os.RemoveAll(am.confFile.Name()); err != nil { am.t.Errorf("Error removing test config file %q: %v", am.confFile.Name(), err) } } // Push declares alerts that are to be pushed to the Alertmanager // server at a relative point in time. func (am *Alertmanager) Push(at float64, alerts ...*TestAlert) { var cas []client.Alert for i := range alerts { a := alerts[i].nativeAlert(am.opts) al := client.Alert{ Labels: client.LabelSet{}, Annotations: client.LabelSet{}, StartsAt: a.StartsAt, EndsAt: a.EndsAt, GeneratorURL: a.GeneratorURL, } for n, v := range a.Labels { al.Labels[client.LabelName(n)] = client.LabelValue(v) } for n, v := range a.Annotations { al.Annotations[client.LabelName(n)] = client.LabelValue(v) } cas = append(cas, al) } alertAPI := client.NewAlertAPI(am.client) am.t.Do(at, func() { if err := alertAPI.Push(context.Background(), cas...); err != nil { am.t.Errorf("Error pushing %v: %s", cas, err) } }) } // SetSilence updates or creates the given Silence. func (am *Alertmanager) SetSilence(at float64, sil *TestSilence) { am.t.Do(at, func() { var buf bytes.Buffer if err := json.NewEncoder(&buf).Encode(sil.nativeSilence(am.opts)); err != nil { am.t.Errorf("Error setting silence %v: %s", sil, err) return } resp, err := http.Post(fmt.Sprintf("http://%s/api/v1/silences", am.apiAddr), "application/json", &buf) if err != nil { am.t.Errorf("Error setting silence %v: %s", sil, err) return } defer resp.Body.Close() b, err := ioutil.ReadAll(resp.Body) if err != nil { panic(err) } var v struct { Status string `json:"status"` Data struct { SilenceID string `json:"silenceId"` } `json:"data"` } if err := json.Unmarshal(b, &v); err != nil || resp.StatusCode/100 != 2 { am.t.Errorf("error setting silence %v: %s", sil, err) return } sil.SetID(v.Data.SilenceID) }) } // DelSilence deletes the silence with the sid at the given time. func (am *Alertmanager) DelSilence(at float64, sil *TestSilence) { am.t.Do(at, func() { req, err := http.NewRequest("DELETE", fmt.Sprintf("http://%s/api/v1/silence/%s", am.apiAddr, sil.ID()), nil) if err != nil { am.t.Errorf("Error deleting silence %v: %s", sil, err) return } resp, err := http.DefaultClient.Do(req) if err != nil || resp.StatusCode/100 != 2 { am.t.Errorf("Error deleting silence %v: %s", sil, err) return } }) } // UpdateConfig rewrites the configuration file for the Alertmanager. It does not // initiate config reloading. func (am *Alertmanager) UpdateConfig(conf string) { if _, err := am.confFile.WriteString(conf); err != nil { am.t.Fatal(err) return } if err := am.confFile.Sync(); err != nil { am.t.Fatal(err) return } } prometheus-alertmanager-0.15.3+ds/test/acceptance/000077500000000000000000000000001341674552200221135ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/test/acceptance/inhibit_test.go000066400000000000000000000104341341674552200251310ustar00rootroot00000000000000// Copyright 2015 Prometheus Team // 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 test import ( "fmt" "testing" "time" . "github.com/prometheus/alertmanager/test" ) func TestInhibiting(t *testing.T) { t.Parallel() // This integration test checks that alerts can be inhibited and that an // inhibited alert will be notified again as soon as the inhibiting alert // gets resolved. conf := ` route: receiver: "default" group_by: [] group_wait: 1s group_interval: 1s repeat_interval: 1s receivers: - name: "default" webhook_configs: - url: 'http://%s' inhibit_rules: - source_match: alertname: JobDown target_match: alertname: InstanceDown equal: - job - zone ` at := NewAcceptanceTest(t, &AcceptanceOpts{ Tolerance: 150 * time.Millisecond, }) co := at.Collector("webhook") wh := NewWebhook(co) am := at.Alertmanager(fmt.Sprintf(conf, wh.Address())) am.Push(At(1), Alert("alertname", "test1", "job", "testjob", "zone", "aa")) am.Push(At(1), Alert("alertname", "InstanceDown", "job", "testjob", "zone", "aa")) am.Push(At(1), Alert("alertname", "InstanceDown", "job", "testjob", "zone", "ab")) // This JobDown in zone aa should inhibit InstanceDown in zone aa in the // second batch of notifications. am.Push(At(2.2), Alert("alertname", "JobDown", "job", "testjob", "zone", "aa")) // InstanceDown in zone aa should fire again in the third batch of // notifications once JobDown in zone aa gets resolved. am.Push(At(3.6), Alert("alertname", "JobDown", "job", "testjob", "zone", "aa").Active(2.2, 3.6)) co.Want(Between(2, 2.5), Alert("alertname", "test1", "job", "testjob", "zone", "aa").Active(1), Alert("alertname", "InstanceDown", "job", "testjob", "zone", "aa").Active(1), Alert("alertname", "InstanceDown", "job", "testjob", "zone", "ab").Active(1), ) co.Want(Between(3, 3.5), Alert("alertname", "test1", "job", "testjob", "zone", "aa").Active(1), Alert("alertname", "InstanceDown", "job", "testjob", "zone", "ab").Active(1), Alert("alertname", "JobDown", "job", "testjob", "zone", "aa").Active(2.2), ) co.Want(Between(4, 4.5), Alert("alertname", "test1", "job", "testjob", "zone", "aa").Active(1), Alert("alertname", "InstanceDown", "job", "testjob", "zone", "aa").Active(1), Alert("alertname", "InstanceDown", "job", "testjob", "zone", "ab").Active(1), Alert("alertname", "JobDown", "job", "testjob", "zone", "aa").Active(2.2, 3.6), ) at.Run() } func TestAlwaysInhibiting(t *testing.T) { t.Parallel() // This integration test checks that when inhibited and inhibiting alerts // gets resolved at the same time, the final notification contains both // alerts. conf := ` route: receiver: "default" group_by: [] group_wait: 1s group_interval: 1s repeat_interval: 1s receivers: - name: "default" webhook_configs: - url: 'http://%s' inhibit_rules: - source_match: alertname: JobDown target_match: alertname: InstanceDown equal: - job - zone ` at := NewAcceptanceTest(t, &AcceptanceOpts{ Tolerance: 150 * time.Millisecond, }) co := at.Collector("webhook") wh := NewWebhook(co) am := at.Alertmanager(fmt.Sprintf(conf, wh.Address())) am.Push(At(1), Alert("alertname", "InstanceDown", "job", "testjob", "zone", "aa")) am.Push(At(1), Alert("alertname", "JobDown", "job", "testjob", "zone", "aa")) am.Push(At(2.6), Alert("alertname", "JobDown", "job", "testjob", "zone", "aa").Active(1, 2.6)) am.Push(At(2.6), Alert("alertname", "InstanceDown", "job", "testjob", "zone", "aa").Active(1, 2.6)) co.Want(Between(2, 2.5), Alert("alertname", "JobDown", "job", "testjob", "zone", "aa").Active(1), ) co.Want(Between(3, 3.5), Alert("alertname", "InstanceDown", "job", "testjob", "zone", "aa").Active(1, 2.6), Alert("alertname", "JobDown", "job", "testjob", "zone", "aa").Active(1, 2.6), ) at.Run() } prometheus-alertmanager-0.15.3+ds/test/acceptance/send_test.go000066400000000000000000000276531341674552200244470ustar00rootroot00000000000000// Copyright 2015 Prometheus Team // 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 test import ( "fmt" "sync" "testing" "time" . "github.com/prometheus/alertmanager/test" ) // This file contains acceptance tests around the basic sending logic // for notifications, which includes batching and ensuring that each // notification is eventually sent at least once and ideally exactly // once. func TestMergeAlerts(t *testing.T) { t.Parallel() conf := ` route: receiver: "default" group_by: [alertname] group_wait: 1s group_interval: 1s repeat_interval: 1ms receivers: - name: "default" webhook_configs: - url: 'http://%s' send_resolved: true ` at := NewAcceptanceTest(t, &AcceptanceOpts{ Tolerance: 150 * time.Millisecond, }) co := at.Collector("webhook") wh := NewWebhook(co) am := at.Alertmanager(fmt.Sprintf(conf, wh.Address())) // Refresh an alert several times. The starting time must remain at the earliest // point in time. am.Push(At(1), Alert("alertname", "test").Active(1.1)) // Another Prometheus server might be sending later but with an earlier start time. am.Push(At(1.2), Alert("alertname", "test").Active(1)) co.Want(Between(2, 2.5), Alert("alertname", "test").Active(1)) am.Push(At(2.1), Alert("alertname", "test").Annotate("ann", "v1").Active(2)) co.Want(Between(3, 3.5), Alert("alertname", "test").Annotate("ann", "v1").Active(1)) // Annotations are always overwritten by the alert that arrived most recently. am.Push(At(3.6), Alert("alertname", "test").Annotate("ann", "v2").Active(1.5)) co.Want(Between(4, 4.5), Alert("alertname", "test").Annotate("ann", "v2").Active(1)) // If an alert is marked resolved twice, the latest point in time must be // set as the eventual resolve time. am.Push(At(4.6), Alert("alertname", "test").Annotate("ann", "v2").Active(3, 4.5)) am.Push(At(4.8), Alert("alertname", "test").Annotate("ann", "v3").Active(2.9, 4.8)) am.Push(At(4.8), Alert("alertname", "test").Annotate("ann", "v3").Active(2.9, 4.1)) co.Want(Between(5, 5.5), Alert("alertname", "test").Annotate("ann", "v3").Active(1, 4.8)) // Reactivate an alert after a previous occurrence has been resolved. // No overlap, no merge must occur. am.Push(At(5.3), Alert("alertname", "test")) co.Want(Between(6, 6.5), Alert("alertname", "test").Active(5.3)) // Test against a bug which ocurrec after a restart. The previous occurrence of // the alert was sent rather than the most recent one. // // XXX(fabxc) disabled as notification info won't be persisted. Thus, with a mesh // notifier we lose the state in this single-node setup. //at.Do(At(6.7), func() { // am.Terminate() // am.Start() //}) // On restart the alert is flushed right away as the group_wait has already passed. // However, it must be caught in the deduplication stage. // The next attempt will be 1s later and won't be filtered in deduping. //co.Want(Between(7.7, 8), Alert("alertname", "test").Active(5.3)) at.Run() } func TestRepeat(t *testing.T) { t.Parallel() conf := ` route: receiver: "default" group_by: [alertname] group_wait: 1s group_interval: 1s repeat_interval: 1ms receivers: - name: "default" webhook_configs: - url: 'http://%s' ` // Create a new acceptance test that instantiates new Alertmanagers // with the given configuration and verifies times with the given // tollerance. at := NewAcceptanceTest(t, &AcceptanceOpts{ Tolerance: 150 * time.Millisecond, }) // Create a collector to which alerts can be written and verified // against a set of expected alert notifications. co := at.Collector("webhook") // Run something that satisfies the webhook interface to which the // Alertmanager pushes as defined by its configuration. wh := NewWebhook(co) // Create a new Alertmanager process listening to a random port am := at.Alertmanager(fmt.Sprintf(conf, wh.Address())) // Declare pushes to be made to the Alertmanager at the given time. // Times are provided in fractions of seconds. am.Push(At(1), Alert("alertname", "test").Active(1)) // XXX(fabxc): disabled as long as alerts are not persisted. // at.Do(At(1.2), func() { // am.Terminate() // am.Start() // }) am.Push(At(3.5), Alert("alertname", "test").Active(1, 3)) // Declare which alerts are expected to arrive at the collector within // the defined time intervals. co.Want(Between(2, 2.5), Alert("alertname", "test").Active(1)) co.Want(Between(3, 3.5), Alert("alertname", "test").Active(1)) co.Want(Between(4, 4.5), Alert("alertname", "test").Active(1, 3)) // Start the flow as defined above and run the checks afterwards. at.Run() } func TestRetry(t *testing.T) { t.Parallel() // We create a notification config that fans out into two different // webhooks. // The succeeding one must still only receive the first successful // notifications. Sending to the succeeding one must eventually succeed. conf := ` route: receiver: "default" group_by: [alertname] group_wait: 1s group_interval: 1s repeat_interval: 3s receivers: - name: "default" webhook_configs: - url: 'http://%s' - url: 'http://%s' ` at := NewAcceptanceTest(t, &AcceptanceOpts{ Tolerance: 150 * time.Millisecond, }) co1 := at.Collector("webhook") wh1 := NewWebhook(co1) co2 := at.Collector("webhook_failing") wh2 := NewWebhook(co2) wh2.Func = func(ts float64) bool { // Fail the first two interval periods but eventually // succeed in the third interval after a few failed attempts. return ts < 4.5 } am := at.Alertmanager(fmt.Sprintf(conf, wh1.Address(), wh2.Address())) am.Push(At(1), Alert("alertname", "test1")) co1.Want(Between(2, 2.5), Alert("alertname", "test1").Active(1)) co1.Want(Between(5, 5.5), Alert("alertname", "test1").Active(1)) co2.Want(Between(4.5, 5), Alert("alertname", "test1").Active(1)) } func TestBatching(t *testing.T) { t.Parallel() conf := ` route: receiver: "default" group_by: [] group_wait: 1s group_interval: 1s # use a value slightly below the 5s interval to avoid timing issues repeat_interval: 4900ms receivers: - name: "default" webhook_configs: - url: 'http://%s' ` at := NewAcceptanceTest(t, &AcceptanceOpts{ Tolerance: 150 * time.Millisecond, }) co := at.Collector("webhook") wh := NewWebhook(co) am := at.Alertmanager(fmt.Sprintf(conf, wh.Address())) am.Push(At(1.1), Alert("alertname", "test1").Active(1)) am.Push(At(1.7), Alert("alertname", "test5").Active(1)) co.Want(Between(2.0, 2.5), Alert("alertname", "test1").Active(1), Alert("alertname", "test5").Active(1), ) am.Push(At(3.3), Alert("alertname", "test2").Active(1.5), Alert("alertname", "test3").Active(1.5), Alert("alertname", "test4").Active(1.6), ) co.Want(Between(4.1, 4.5), Alert("alertname", "test1").Active(1), Alert("alertname", "test5").Active(1), Alert("alertname", "test2").Active(1.5), Alert("alertname", "test3").Active(1.5), Alert("alertname", "test4").Active(1.6), ) // While no changes happen expect no additional notifications // until the 5s repeat interval has ended. co.Want(Between(9.1, 9.5), Alert("alertname", "test1").Active(1), Alert("alertname", "test5").Active(1), Alert("alertname", "test2").Active(1.5), Alert("alertname", "test3").Active(1.5), Alert("alertname", "test4").Active(1.6), ) at.Run() } func TestResolved(t *testing.T) { t.Parallel() var wg sync.WaitGroup wg.Add(10) for i := 0; i < 10; i++ { go func() { conf := ` global: resolve_timeout: 10s route: receiver: "default" group_by: [alertname] group_wait: 1s group_interval: 5s receivers: - name: "default" webhook_configs: - url: 'http://%s' ` at := NewAcceptanceTest(t, &AcceptanceOpts{ Tolerance: 150 * time.Millisecond, }) co := at.Collector("webhook") wh := NewWebhook(co) am := at.Alertmanager(fmt.Sprintf(conf, wh.Address())) am.Push(At(1), Alert("alertname", "test", "lbl", "v1"), Alert("alertname", "test", "lbl", "v2"), Alert("alertname", "test", "lbl", "v3"), ) co.Want(Between(2, 2.5), Alert("alertname", "test", "lbl", "v1").Active(1), Alert("alertname", "test", "lbl", "v2").Active(1), Alert("alertname", "test", "lbl", "v3").Active(1), ) co.Want(Between(12, 13), Alert("alertname", "test", "lbl", "v1").Active(1, 11), Alert("alertname", "test", "lbl", "v2").Active(1, 11), Alert("alertname", "test", "lbl", "v3").Active(1, 11), ) at.Run() wg.Done() }() } wg.Wait() } func TestResolvedFilter(t *testing.T) { t.Parallel() // This integration test ensures that even though resolved alerts may not be // notified about, they must be set as notified. Resolved alerts, even when // filtered, have to end up in the SetNotifiesStage, otherwise when an alert // fires again it is ambiguous whether it was resolved in between or not. conf := ` global: resolve_timeout: 10s route: receiver: "default" group_by: [alertname] group_wait: 1s group_interval: 5s receivers: - name: "default" webhook_configs: - url: 'http://%s' send_resolved: true - url: 'http://%s' send_resolved: false ` at := NewAcceptanceTest(t, &AcceptanceOpts{ Tolerance: 150 * time.Millisecond, }) co1 := at.Collector("webhook1") wh1 := NewWebhook(co1) co2 := at.Collector("webhook2") wh2 := NewWebhook(co2) am := at.Alertmanager(fmt.Sprintf(conf, wh1.Address(), wh2.Address())) am.Push(At(1), Alert("alertname", "test", "lbl", "v1"), Alert("alertname", "test", "lbl", "v2"), ) am.Push(At(3), Alert("alertname", "test", "lbl", "v1").Active(1, 4), Alert("alertname", "test", "lbl", "v3"), ) am.Push(At(8), Alert("alertname", "test", "lbl", "v3").Active(3), ) co1.Want(Between(2, 2.5), Alert("alertname", "test", "lbl", "v1").Active(1), Alert("alertname", "test", "lbl", "v2").Active(1), ) co1.Want(Between(7, 7.5), Alert("alertname", "test", "lbl", "v1").Active(1, 4), Alert("alertname", "test", "lbl", "v2").Active(1), Alert("alertname", "test", "lbl", "v3").Active(3), ) // Notification should be sent because the v2 alert is resolved due to the time-out. co1.Want(Between(12, 12.5), Alert("alertname", "test", "lbl", "v2").Active(1, 11), Alert("alertname", "test", "lbl", "v3").Active(3), ) co2.Want(Between(2, 2.5), Alert("alertname", "test", "lbl", "v1").Active(1), Alert("alertname", "test", "lbl", "v2").Active(1), ) co2.Want(Between(7, 7.5), Alert("alertname", "test", "lbl", "v2").Active(1), Alert("alertname", "test", "lbl", "v3").Active(3), ) // No notification should be sent after group_interval because no new alert has been fired. co2.Want(Between(12, 12.5)) at.Run() } func TestReload(t *testing.T) { t.Parallel() // This integration test ensures that the first alert isn't notified twice // and repeat_interval applies after the AlertManager process has been // reloaded. conf := ` route: receiver: "default" group_by: [] group_wait: 1s group_interval: 6s repeat_interval: 10m receivers: - name: "default" webhook_configs: - url: 'http://%s' ` at := NewAcceptanceTest(t, &AcceptanceOpts{ Tolerance: 150 * time.Millisecond, }) co := at.Collector("webhook") wh := NewWebhook(co) am := at.Alertmanager(fmt.Sprintf(conf, wh.Address())) am.Push(At(1), Alert("alertname", "test1")) at.Do(At(3), am.Reload) am.Push(At(4), Alert("alertname", "test2")) co.Want(Between(2, 2.5), Alert("alertname", "test1").Active(1)) // Timers are reset on reload regardless, so we count the 6 second group // interval from 3 onwards. co.Want(Between(9, 9.5), Alert("alertname", "test1").Active(1), Alert("alertname", "test2").Active(4), ) at.Run() } prometheus-alertmanager-0.15.3+ds/test/acceptance/silence_test.go000066400000000000000000000054631341674552200251330ustar00rootroot00000000000000// Copyright 2015 Prometheus Team // 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 test import ( "fmt" "testing" "time" . "github.com/prometheus/alertmanager/test" ) func TestSilencing(t *testing.T) { t.Parallel() conf := ` route: receiver: "default" group_by: [] group_wait: 1s group_interval: 1s repeat_interval: 1ms receivers: - name: "default" webhook_configs: - url: 'http://%s' ` at := NewAcceptanceTest(t, &AcceptanceOpts{ Tolerance: 150 * time.Millisecond, }) co := at.Collector("webhook") wh := NewWebhook(co) am := at.Alertmanager(fmt.Sprintf(conf, wh.Address())) // No repeat interval is configured. Thus, we receive an alert // notification every second. am.Push(At(1), Alert("alertname", "test1").Active(1)) am.Push(At(1), Alert("alertname", "test2").Active(1)) co.Want(Between(2, 2.5), Alert("alertname", "test1").Active(1), Alert("alertname", "test2").Active(1), ) // Add a silence that affects the first alert. am.SetSilence(At(2.3), Silence(2.5, 4.5).Match("alertname", "test1")) co.Want(Between(3, 3.5), Alert("alertname", "test2").Active(1)) co.Want(Between(4, 4.5), Alert("alertname", "test2").Active(1)) // Silence should be over now and we receive both alerts again. co.Want(Between(5, 5.5), Alert("alertname", "test1").Active(1), Alert("alertname", "test2").Active(1), ) at.Run() } func TestSilenceDelete(t *testing.T) { t.Parallel() conf := ` route: receiver: "default" group_by: [] group_wait: 1s group_interval: 1s repeat_interval: 1ms receivers: - name: "default" webhook_configs: - url: 'http://%s' ` at := NewAcceptanceTest(t, &AcceptanceOpts{ Tolerance: 150 * time.Millisecond, }) co := at.Collector("webhook") wh := NewWebhook(co) am := at.Alertmanager(fmt.Sprintf(conf, wh.Address())) // No repeat interval is configured. Thus, we receive an alert // notification every second. am.Push(At(1), Alert("alertname", "test1").Active(1)) am.Push(At(1), Alert("alertname", "test2").Active(1)) // Silence everything for a long time and delete the silence after // two iterations. sil := Silence(1.5, 100).MatchRE("alertname", ".*") am.SetSilence(At(1.3), sil) am.DelSilence(At(3.5), sil) co.Want(Between(3.5, 4.5), Alert("alertname", "test1").Active(1), Alert("alertname", "test2").Active(1), ) at.Run() } prometheus-alertmanager-0.15.3+ds/test/collector.go000066400000000000000000000073001341674552200223420ustar00rootroot00000000000000// Copyright 2015 Prometheus Team // 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 test import ( "fmt" "sync" "testing" "time" "github.com/prometheus/common/model" ) // Collector gathers alerts received by a notification receiver // and verifies whether all arrived and within the correct time boundaries. type Collector struct { t *testing.T name string opts *AcceptanceOpts collected map[float64][]model.Alerts expected map[Interval][]model.Alerts mtx sync.RWMutex } func (c *Collector) String() string { return c.name } func batchesEqual(as, bs model.Alerts, opts *AcceptanceOpts) bool { if len(as) != len(bs) { return false } for _, a := range as { found := false for _, b := range bs { if equalAlerts(a, b, opts) { found = true break } } if !found { return false } } return true } // latest returns the latest relative point in time where a notification is // expected. func (c *Collector) latest() float64 { c.mtx.RLock() defer c.mtx.RUnlock() var latest float64 for iv := range c.expected { if iv.end > latest { latest = iv.end } } return latest } // Want declares that the Collector expects to receive the given alerts // within the given time boundaries. func (c *Collector) Want(iv Interval, alerts ...*TestAlert) { c.mtx.Lock() defer c.mtx.Unlock() var nas model.Alerts for _, a := range alerts { nas = append(nas, a.nativeAlert(c.opts)) } c.expected[iv] = append(c.expected[iv], nas) } // add the given alerts to the collected alerts. func (c *Collector) add(alerts ...*model.Alert) { c.mtx.Lock() defer c.mtx.Unlock() arrival := c.opts.relativeTime(time.Now()) c.collected[arrival] = append(c.collected[arrival], model.Alerts(alerts)) } func (c *Collector) check() string { report := fmt.Sprintf("\ncollector %q:\n\n", c) c.mtx.RLock() defer c.mtx.RUnlock() for iv, expected := range c.expected { report += fmt.Sprintf("interval %v\n", iv) var alerts []model.Alerts for at, got := range c.collected { if iv.contains(at) { alerts = append(alerts, got...) } } for _, exp := range expected { found := len(exp) == 0 && len(alerts) == 0 report += fmt.Sprintf("---\n") for _, e := range exp { report += fmt.Sprintf("- %v\n", c.opts.alertString(e)) } for _, a := range alerts { if batchesEqual(exp, a, c.opts) { found = true break } } if found { report += fmt.Sprintf(" [ ✓ ]\n") } else { c.t.Fail() report += fmt.Sprintf(" [ ✗ ]\n") } } } // Detect unexpected notifications. var totalExp, totalAct int for _, exp := range c.expected { for _, e := range exp { totalExp += len(e) } } for _, act := range c.collected { for _, a := range act { if len(a) == 0 { c.t.Error("received empty notifications") } totalAct += len(a) } } if totalExp != totalAct { c.t.Fail() report += fmt.Sprintf("\nExpected total of %d alerts, got %d", totalExp, totalAct) } if c.t.Failed() { report += "\nreceived:\n" for at, col := range c.collected { for _, alerts := range col { report += fmt.Sprintf("@ %v\n", at) for _, a := range alerts { report += fmt.Sprintf("- %v\n", c.opts.alertString(a)) } } } } return report } prometheus-alertmanager-0.15.3+ds/test/mock.go000066400000000000000000000155631341674552200213170ustar00rootroot00000000000000// Copyright 2015 Prometheus Team // 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 test import ( "encoding/json" "fmt" "net" "net/http" "reflect" "sync" "time" "github.com/prometheus/common/model" "github.com/prometheus/alertmanager/notify" "github.com/prometheus/alertmanager/types" ) // At is a convenience method to allow for declarative syntax of Acceptance // test definitions. func At(ts float64) float64 { return ts } type Interval struct { start, end float64 } func (iv Interval) String() string { return fmt.Sprintf("[%v,%v]", iv.start, iv.end) } func (iv Interval) contains(f float64) bool { return f >= iv.start && f <= iv.end } // Between is a convenience constructor for an interval for declarative syntax // of Acceptance test definitions. func Between(start, end float64) Interval { return Interval{start: start, end: end} } // TestSilence models a model.Silence with relative times. type TestSilence struct { id string match []string matchRE []string startsAt, endsAt float64 mtx sync.RWMutex } // Silence creates a new TestSilence active for the relative interval given // by start and end. func Silence(start, end float64) *TestSilence { return &TestSilence{ startsAt: start, endsAt: end, } } // Match adds a new plain matcher to the silence. func (s *TestSilence) Match(v ...string) *TestSilence { s.match = append(s.match, v...) return s } // MatchRE adds a new regex matcher to the silence func (s *TestSilence) MatchRE(v ...string) *TestSilence { if len(v)%2 == 1 { panic("bad key/values") } s.matchRE = append(s.matchRE, v...) return s } // SetID sets the silence ID. func (s *TestSilence) SetID(ID string) { s.mtx.Lock() defer s.mtx.Unlock() s.id = ID } // ID gets the silence ID. func (s *TestSilence) ID() string { s.mtx.RLock() defer s.mtx.RUnlock() return s.id } // nativeSilence converts the declared test silence into a regular // silence with resolved times. func (s *TestSilence) nativeSilence(opts *AcceptanceOpts) *types.Silence { nsil := &types.Silence{} for i := 0; i < len(s.match); i += 2 { nsil.Matchers = append(nsil.Matchers, &types.Matcher{ Name: s.match[i], Value: s.match[i+1], }) } for i := 0; i < len(s.matchRE); i += 2 { nsil.Matchers = append(nsil.Matchers, &types.Matcher{ Name: s.matchRE[i], Value: s.matchRE[i+1], IsRegex: true, }) } if s.startsAt > 0 { nsil.StartsAt = opts.expandTime(s.startsAt) } if s.endsAt > 0 { nsil.EndsAt = opts.expandTime(s.endsAt) } nsil.Comment = "some comment" nsil.CreatedBy = "admin@example.com" return nsil } // TestAlert models a model.Alert with relative times. type TestAlert struct { labels model.LabelSet annotations model.LabelSet startsAt, endsAt float64 } // Alert creates a new alert declaration with the given key/value pairs // as identifying labels. func Alert(keyval ...interface{}) *TestAlert { if len(keyval)%2 == 1 { panic("bad key/values") } a := &TestAlert{ labels: model.LabelSet{}, annotations: model.LabelSet{}, } for i := 0; i < len(keyval); i += 2 { ln := model.LabelName(keyval[i].(string)) lv := model.LabelValue(keyval[i+1].(string)) a.labels[ln] = lv } return a } // nativeAlert converts the declared test alert into a full alert based // on the given paramters. func (a *TestAlert) nativeAlert(opts *AcceptanceOpts) *model.Alert { na := &model.Alert{ Labels: a.labels, Annotations: a.annotations, } if a.startsAt > 0 { na.StartsAt = opts.expandTime(a.startsAt) } if a.endsAt > 0 { na.EndsAt = opts.expandTime(a.endsAt) } return na } // Annotate the alert with the given key/value pairs. func (a *TestAlert) Annotate(keyval ...interface{}) *TestAlert { if len(keyval)%2 == 1 { panic("bad key/values") } for i := 0; i < len(keyval); i += 2 { ln := model.LabelName(keyval[i].(string)) lv := model.LabelValue(keyval[i+1].(string)) a.annotations[ln] = lv } return a } // Active declares the relative activity time for this alert. It // must be a single starting value or two values where the second value // declares the resolved time. func (a *TestAlert) Active(tss ...float64) *TestAlert { if len(tss) > 2 || len(tss) == 0 { panic("only one or two timestamps allowed") } if len(tss) == 2 { a.endsAt = tss[1] } a.startsAt = tss[0] return a } func equalAlerts(a, b *model.Alert, opts *AcceptanceOpts) bool { if !reflect.DeepEqual(a.Labels, b.Labels) { return false } if !reflect.DeepEqual(a.Annotations, b.Annotations) { return false } if !equalTime(a.StartsAt, b.StartsAt, opts) { return false } if !equalTime(a.EndsAt, b.EndsAt, opts) { return false } return true } func equalTime(a, b time.Time, opts *AcceptanceOpts) bool { if a.IsZero() != b.IsZero() { return false } diff := a.Sub(b) if diff < 0 { diff = -diff } return diff <= opts.Tolerance } type MockWebhook struct { opts *AcceptanceOpts collector *Collector listener net.Listener Func func(timestamp float64) bool } func NewWebhook(c *Collector) *MockWebhook { l, err := net.Listen("tcp4", "localhost:0") if err != nil { // TODO(fabxc): if shutdown of mock destinations ever becomes a concern // we want to shut them down after test completion. Then we might want to // log the error properly, too. panic(err) } wh := &MockWebhook{ listener: l, collector: c, opts: c.opts, } go http.Serve(l, wh) return wh } func (ws *MockWebhook) ServeHTTP(w http.ResponseWriter, req *http.Request) { // Inject Func if it exists. if ws.Func != nil { if ws.Func(ws.opts.relativeTime(time.Now())) { return } } dec := json.NewDecoder(req.Body) defer req.Body.Close() var v notify.WebhookMessage if err := dec.Decode(&v); err != nil { panic(err) } // Transform the webhook message alerts back into model.Alerts. var alerts model.Alerts for _, a := range v.Alerts { var ( labels = model.LabelSet{} annotations = model.LabelSet{} ) for k, v := range a.Labels { labels[model.LabelName(k)] = model.LabelValue(v) } for k, v := range a.Annotations { annotations[model.LabelName(k)] = model.LabelValue(v) } alerts = append(alerts, &model.Alert{ Labels: labels, Annotations: annotations, StartsAt: a.StartsAt, EndsAt: a.EndsAt, GeneratorURL: a.GeneratorURL, }) } ws.collector.add(alerts...) } func (ws *MockWebhook) Address() string { return ws.listener.Addr().String() } prometheus-alertmanager-0.15.3+ds/types/000077500000000000000000000000001341674552200202125ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/types/match.go000066400000000000000000000100121341674552200216270ustar00rootroot00000000000000// Copyright 2015 Prometheus Team // 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 types import ( "fmt" "regexp" "sort" "bytes" "github.com/prometheus/common/model" ) // Matcher defines a matching rule for the value of a given label. type Matcher struct { Name string `json:"name"` Value string `json:"value"` IsRegex bool `json:"isRegex"` regex *regexp.Regexp } // Init internals of the Matcher. Must be called before using Match. func (m *Matcher) Init() error { if !m.IsRegex { return nil } re, err := regexp.Compile("^(?:" + m.Value + ")$") if err == nil { m.regex = re } return err } func (m *Matcher) String() string { if m.IsRegex { return fmt.Sprintf("%s=~%q", m.Name, m.Value) } return fmt.Sprintf("%s=%q", m.Name, m.Value) } // Validate returns true iff all fields of the matcher have valid values. func (m *Matcher) Validate() error { if !model.LabelName(m.Name).IsValid() { return fmt.Errorf("invalid name %q", m.Name) } if m.IsRegex { if _, err := regexp.Compile(m.Value); err != nil { return fmt.Errorf("invalid regular expression %q", m.Value) } } else if !model.LabelValue(m.Value).IsValid() || len(m.Value) == 0 { return fmt.Errorf("invalid value %q", m.Value) } return nil } // Match checks whether the label of the matcher has the specified // matching value. func (m *Matcher) Match(lset model.LabelSet) bool { // Unset labels are treated as unset labels globally. Thus, if a // label is not set we retrieve the empty label which is correct // for the comparison below. v := lset[model.LabelName(m.Name)] if m.IsRegex { return m.regex.MatchString(string(v)) } return string(v) == m.Value } // NewMatcher returns a new matcher that compares against equality of // the given value. func NewMatcher(name model.LabelName, value string) *Matcher { return &Matcher{ Name: string(name), Value: value, IsRegex: false, } } // NewRegexMatcher returns a new matcher that compares values against // a regular expression. The matcher is already initialized. // // TODO(fabxc): refactor usage. func NewRegexMatcher(name model.LabelName, re *regexp.Regexp) *Matcher { return &Matcher{ Name: string(name), Value: re.String(), IsRegex: true, regex: re, } } // Matchers provides the Match and Fingerprint methods for a slice of Matchers. // Matchers must always be sorted. type Matchers []*Matcher // NewMatchers returns the given Matchers sorted. func NewMatchers(ms ...*Matcher) Matchers { m := Matchers(ms) sort.Sort(m) return m } func (ms Matchers) Len() int { return len(ms) } func (ms Matchers) Swap(i, j int) { ms[i], ms[j] = ms[j], ms[i] } func (ms Matchers) Less(i, j int) bool { if ms[i].Name > ms[j].Name { return false } if ms[i].Name < ms[j].Name { return true } if ms[i].Value > ms[j].Value { return false } if ms[i].Value < ms[j].Value { return true } return !ms[i].IsRegex && ms[j].IsRegex } // Equal returns whether both Matchers are equal. func (ms Matchers) Equal(o Matchers) bool { if len(ms) != len(o) { return false } for i, a := range ms { if *a != *o[i] { return false } } return true } // Match checks whether all matchers are fulfilled against the given label set. func (ms Matchers) Match(lset model.LabelSet) bool { for _, m := range ms { if !m.Match(lset) { return false } } return true } func (ms Matchers) String() string { var buf bytes.Buffer buf.WriteByte('{') for i, m := range ms { if i > 0 { buf.WriteByte(',') } buf.WriteString(m.String()) } buf.WriteByte('}') return buf.String() } prometheus-alertmanager-0.15.3+ds/types/match_test.go000066400000000000000000000114401341674552200226740ustar00rootroot00000000000000// Copyright 2018 Prometheus Team // 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 types import ( "fmt" "regexp" "testing" "github.com/prometheus/common/model" "github.com/stretchr/testify/require" ) func TestMatcherValidate(t *testing.T) { validLabelName := "valid_label_name" validStringValue := "value" validRegexValue := ".*" invalidLabelName := "123_invalid_name" invalidStringValue := "" invalidRegexValue := "]*.[" tests := []struct { matcher Matcher valid bool errorMsg string }{ //valid tests { matcher: Matcher{Name: validLabelName, Value: validStringValue}, valid: true, }, { matcher: Matcher{Name: validLabelName, Value: validRegexValue, IsRegex: true}, valid: true, }, // invalid tests { matcher: Matcher{Name: invalidLabelName, Value: validStringValue}, valid: false, errorMsg: fmt.Sprintf("invalid name %q", invalidLabelName), }, { matcher: Matcher{Name: validLabelName, Value: invalidStringValue}, valid: false, errorMsg: fmt.Sprintf("invalid value %q", invalidStringValue), }, { matcher: Matcher{Name: validLabelName, Value: invalidRegexValue, IsRegex: true}, valid: false, errorMsg: fmt.Sprintf("invalid regular expression %q", invalidRegexValue), }, } for _, test := range tests { test.matcher.Init() if test.valid { require.NoError(t, test.matcher.Validate()) continue } require.EqualError(t, test.matcher.Validate(), test.errorMsg) } } func TestMatcherInit(t *testing.T) { m := Matcher{Name: "label", Value: ".*", IsRegex: true} require.NoError(t, m.Init()) require.EqualValues(t, "^(?:.*)$", m.regex.String()) m = Matcher{Name: "label", Value: "]*.[", IsRegex: true} require.Error(t, m.Init()) } func TestMatcherMatch(t *testing.T) { tests := []struct { matcher Matcher expected bool }{ {matcher: Matcher{Name: "label", Value: "value"}, expected: true}, {matcher: Matcher{Name: "label", Value: "val"}, expected: false}, {matcher: Matcher{Name: "label", Value: "val.*", IsRegex: true}, expected: true}, {matcher: Matcher{Name: "label", Value: "diffval.*", IsRegex: true}, expected: false}, //unset label {matcher: Matcher{Name: "difflabel", Value: "value"}, expected: false}, } lset := model.LabelSet{"label": "value"} for _, test := range tests { test.matcher.Init() actual := test.matcher.Match(lset) require.EqualValues(t, test.expected, actual) } } func TestMatcherString(t *testing.T) { m := NewMatcher("foo", "bar") if m.String() != "foo=\"bar\"" { t.Errorf("unexpected matcher string %#v", m.String()) } re, err := regexp.Compile(".*") if err != nil { t.Errorf("unexpected error: %s", err) } m = NewRegexMatcher("foo", re) if m.String() != "foo=~\".*\"" { t.Errorf("unexpected matcher string %#v", m.String()) } } func TestMatchersString(t *testing.T) { m1 := NewMatcher("foo", "bar") re, err := regexp.Compile(".*") if err != nil { t.Errorf("unexpected error: %s", err) } m2 := NewRegexMatcher("bar", re) matchers := NewMatchers(m1, m2) if matchers.String() != "{bar=~\".*\",foo=\"bar\"}" { t.Errorf("unexpected matcher string %#v", matchers.String()) } } func TestMatchersMatch(t *testing.T) { m1 := &Matcher{Name: "label1", Value: "value1"} m1.Init() m2 := &Matcher{Name: "label2", Value: "val.*", IsRegex: true} m2.Init() m3 := &Matcher{Name: "label3", Value: "value3"} m3.Init() tests := []struct { matchers Matchers expected bool }{ {matchers: Matchers{m1, m2}, expected: true}, {matchers: Matchers{m1, m3}, expected: false}, } lset := model.LabelSet{"label1": "value1", "label2": "value2"} for _, test := range tests { actual := test.matchers.Match(lset) require.EqualValues(t, test.expected, actual) } } func TestMatchersEqual(t *testing.T) { m1 := &Matcher{Name: "label1", Value: "value1"} m1.Init() m2 := &Matcher{Name: "label2", Value: "val.*", IsRegex: true} m2.Init() m3 := &Matcher{Name: "label3", Value: "value3"} m3.Init() tests := []struct { matchers1 Matchers matchers2 Matchers expected bool }{ {matchers1: Matchers{m1, m2}, matchers2: Matchers{m1, m2}, expected: true}, {matchers1: Matchers{m1, m3}, matchers2: Matchers{m1, m2}, expected: false}, } for _, test := range tests { actual := test.matchers1.Equal(test.matchers2) require.EqualValues(t, test.expected, actual) } } prometheus-alertmanager-0.15.3+ds/types/types.go000066400000000000000000000241651341674552200217150ustar00rootroot00000000000000// Copyright 2015 Prometheus Team // 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 types import ( "strings" "sync" "time" "github.com/prometheus/common/model" ) type AlertState string const ( AlertStateUnprocessed AlertState = "unprocessed" AlertStateActive AlertState = "active" AlertStateSuppressed AlertState = "suppressed" ) // AlertStatus stores the state and values associated with an Alert. type AlertStatus struct { State AlertState `json:"state"` SilencedBy []string `json:"silencedBy"` InhibitedBy []string `json:"inhibitedBy"` } // Marker helps to mark alerts as silenced and/or inhibited. // All methods are goroutine-safe. type Marker interface { SetActive(alert model.Fingerprint) SetInhibited(alert model.Fingerprint, ids ...string) SetSilenced(alert model.Fingerprint, ids ...string) Count(...AlertState) int Status(model.Fingerprint) AlertStatus Delete(model.Fingerprint) Unprocessed(model.Fingerprint) bool Active(model.Fingerprint) bool Silenced(model.Fingerprint) ([]string, bool) Inhibited(model.Fingerprint) ([]string, bool) } // NewMarker returns an instance of a Marker implementation. func NewMarker() Marker { return &memMarker{ m: map[model.Fingerprint]*AlertStatus{}, } } type memMarker struct { m map[model.Fingerprint]*AlertStatus mtx sync.RWMutex } // Count alerts of a given state. func (m *memMarker) Count(states ...AlertState) int { count := 0 m.mtx.RLock() defer m.mtx.RUnlock() if len(states) == 0 { count = len(m.m) } else { for _, status := range m.m { for _, state := range states { if status.State == state { count++ } } } } return count } // SetSilenced sets the AlertStatus to suppressed and stores the associated silence IDs. func (m *memMarker) SetSilenced(alert model.Fingerprint, ids ...string) { m.mtx.Lock() s, found := m.m[alert] if !found { s = &AlertStatus{} m.m[alert] = s } // If there are any silence or alert IDs associated with the // fingerprint, it is suppressed. Otherwise, set it to // AlertStateUnprocessed. if len(ids) == 0 && len(s.InhibitedBy) == 0 { m.mtx.Unlock() m.SetActive(alert) return } s.State = AlertStateSuppressed s.SilencedBy = ids m.mtx.Unlock() } // SetInhibited sets the AlertStatus to suppressed and stores the associated alert IDs. func (m *memMarker) SetInhibited(alert model.Fingerprint, ids ...string) { m.mtx.Lock() s, found := m.m[alert] if !found { s = &AlertStatus{} m.m[alert] = s } // If there are any silence or alert IDs associated with the // fingerprint, it is suppressed. Otherwise, set it to // AlertStateUnprocessed. if len(ids) == 0 && len(s.SilencedBy) == 0 { m.mtx.Unlock() m.SetActive(alert) return } s.State = AlertStateSuppressed s.InhibitedBy = ids m.mtx.Unlock() } func (m *memMarker) SetActive(alert model.Fingerprint) { m.mtx.Lock() defer m.mtx.Unlock() s, found := m.m[alert] if !found { s = &AlertStatus{ SilencedBy: []string{}, InhibitedBy: []string{}, } m.m[alert] = s } s.State = AlertStateActive s.SilencedBy = []string{} s.InhibitedBy = []string{} } // Status returns the AlertStatus for the given Fingerprint. func (m *memMarker) Status(alert model.Fingerprint) AlertStatus { m.mtx.RLock() defer m.mtx.RUnlock() s, found := m.m[alert] if !found { s = &AlertStatus{ State: AlertStateUnprocessed, SilencedBy: []string{}, InhibitedBy: []string{}, } } return *s } // Delete deletes the given Fingerprint from the internal cache. func (m *memMarker) Delete(alert model.Fingerprint) { m.mtx.Lock() defer m.mtx.Unlock() delete(m.m, alert) } // Unprocessed returns whether the alert for the given Fingerprint is in the // Unprocessed state. func (m *memMarker) Unprocessed(alert model.Fingerprint) bool { return m.Status(alert).State == AlertStateUnprocessed } // Active returns whether the alert for the given Fingerprint is in the Active // state. func (m *memMarker) Active(alert model.Fingerprint) bool { return m.Status(alert).State == AlertStateActive } // Inhibited returns whether the alert for the given Fingerprint is in the // Inhibited state and any associated alert IDs. func (m *memMarker) Inhibited(alert model.Fingerprint) ([]string, bool) { s := m.Status(alert) return s.InhibitedBy, s.State == AlertStateSuppressed && len(s.InhibitedBy) > 0 } // Silenced returns whether the alert for the given Fingerprint is in the // Silenced state and any associated silence IDs. func (m *memMarker) Silenced(alert model.Fingerprint) ([]string, bool) { s := m.Status(alert) return s.SilencedBy, s.State == AlertStateSuppressed && len(s.SilencedBy) > 0 } // MultiError contains multiple errors and implements the error interface. Its // zero value is ready to use. All its methods are goroutine safe. type MultiError struct { mtx sync.Mutex errors []error } // Add adds an error to the MultiError. func (e *MultiError) Add(err error) { e.mtx.Lock() defer e.mtx.Unlock() e.errors = append(e.errors, err) } // Len returns the number of errors added to the MultiError. func (e *MultiError) Len() int { e.mtx.Lock() defer e.mtx.Unlock() return len(e.errors) } // Errors returns the errors added to the MuliError. The returned slice is a // copy of the internal slice of errors. func (e *MultiError) Errors() []error { e.mtx.Lock() defer e.mtx.Unlock() return append(make([]error, 0, len(e.errors)), e.errors...) } func (e *MultiError) Error() string { e.mtx.Lock() defer e.mtx.Unlock() es := make([]string, 0, len(e.errors)) for _, err := range e.errors { es = append(es, err.Error()) } return strings.Join(es, "; ") } // Alert wraps a model.Alert with additional information relevant // to internal of the Alertmanager. // The type is never exposed to external communication and the // embedded alert has to be sanitized beforehand. type Alert struct { model.Alert // The authoritative timestamp. UpdatedAt time.Time Timeout bool } // AlertSlice is a sortable slice of Alerts. type AlertSlice []*Alert func (as AlertSlice) Less(i, j int) bool { // Look at labels.job, then labels.instance. for _, overrideKey := range [...]model.LabelName{"job", "instance"} { iVal, iOk := as[i].Labels[overrideKey] jVal, jOk := as[j].Labels[overrideKey] if !iOk && !jOk { continue } if !iOk { return false } if !jOk { return true } if iVal != jVal { return iVal < jVal } } return as[i].Labels.Before(as[j].Labels) } func (as AlertSlice) Swap(i, j int) { as[i], as[j] = as[j], as[i] } func (as AlertSlice) Len() int { return len(as) } // Alerts turns a sequence of internal alerts into a list of // exposable model.Alert structures. func Alerts(alerts ...*Alert) model.Alerts { res := make(model.Alerts, 0, len(alerts)) for _, a := range alerts { v := a.Alert // If the end timestamp is not reached yet, do not expose it. if !a.Resolved() { v.EndsAt = time.Time{} } res = append(res, &v) } return res } // Merge merges the timespan of two alerts based and overwrites annotations // based on the authoritative timestamp. A new alert is returned, the labels // are assumed to be equal. func (a *Alert) Merge(o *Alert) *Alert { // Let o always be the younger alert. if o.UpdatedAt.Before(a.UpdatedAt) { return o.Merge(a) } res := *o // Always pick the earliest starting time. if a.StartsAt.Before(o.StartsAt) { res.StartsAt = a.StartsAt } if o.Resolved() { // The latest explicit resolved timestamp wins if both alerts are effectively resolved. if a.Resolved() && a.EndsAt.After(o.EndsAt) { res.EndsAt = a.EndsAt } } else { // A non-timeout timestamp always rules if it is the latest. if a.EndsAt.After(o.EndsAt) && !a.Timeout { res.EndsAt = a.EndsAt } } return &res } // A Muter determines whether a given label set is muted. type Muter interface { Mutes(model.LabelSet) bool } // A MuteFunc is a function that implements the Muter interface. type MuteFunc func(model.LabelSet) bool // Mutes implements the Muter interface. func (f MuteFunc) Mutes(lset model.LabelSet) bool { return f(lset) } // A Silence determines whether a given label set is muted. type Silence struct { // A unique identifier across all connected instances. ID string `json:"id"` // A set of matchers determining if a label set is affect // by the silence. Matchers Matchers `json:"matchers"` // Time range of the silence. // // * StartsAt must not be before creation time // * EndsAt must be after StartsAt // * Deleting a silence means to set EndsAt to now // * Time range must not be modified in different ways // // TODO(fabxc): this may potentially be extended by // creation and update timestamps. StartsAt time.Time `json:"startsAt"` EndsAt time.Time `json:"endsAt"` // The last time the silence was updated. UpdatedAt time.Time `json:"updatedAt"` // Information about who created the silence for which reason. CreatedBy string `json:"createdBy"` Comment string `json:"comment,omitempty"` // timeFunc provides the time against which to evaluate // the silence. Used for test injection. now func() time.Time Status SilenceStatus `json:"status"` } // Expired return if the silence is expired // meaning that both StartsAt and EndsAt are equal func (s *Silence) Expired() bool { return s.StartsAt.Equal(s.EndsAt) } type SilenceStatus struct { State SilenceState `json:"state"` } type SilenceState string const ( SilenceStateExpired SilenceState = "expired" SilenceStateActive SilenceState = "active" SilenceStatePending SilenceState = "pending" ) func CalcSilenceState(start, end time.Time) SilenceState { current := time.Now() if current.Before(start) { return SilenceStatePending } if current.Before(end) { return SilenceStateActive } return SilenceStateExpired } prometheus-alertmanager-0.15.3+ds/types/types_test.go000066400000000000000000000205261341674552200227510ustar00rootroot00000000000000// Copyright 2015 Prometheus Team // 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 types import ( "reflect" "sort" "strconv" "testing" "time" "github.com/prometheus/common/model" "github.com/stretchr/testify/require" ) func TestAlertMerge(t *testing.T) { now := time.Now() // By convention, alert A is always older than alert B. pairs := []struct { A, B, Res *Alert }{ { // Both alerts have the Timeout flag set. // StartsAt is defined by Alert A. // EndsAt is defined by Alert B. A: &Alert{ Alert: model.Alert{ StartsAt: now.Add(-2 * time.Minute), EndsAt: now.Add(2 * time.Minute), }, UpdatedAt: now, Timeout: true, }, B: &Alert{ Alert: model.Alert{ StartsAt: now.Add(-time.Minute), EndsAt: now.Add(3 * time.Minute), }, UpdatedAt: now.Add(time.Minute), Timeout: true, }, Res: &Alert{ Alert: model.Alert{ StartsAt: now.Add(-2 * time.Minute), EndsAt: now.Add(3 * time.Minute), }, UpdatedAt: now.Add(time.Minute), Timeout: true, }, }, { // Alert A has the Timeout flag set while Alert B has it unset. // StartsAt is defined by Alert A. // EndsAt is defined by Alert B. A: &Alert{ Alert: model.Alert{ StartsAt: now.Add(-time.Minute), EndsAt: now.Add(3 * time.Minute), }, UpdatedAt: now, Timeout: true, }, B: &Alert{ Alert: model.Alert{ StartsAt: now, EndsAt: now.Add(2 * time.Minute), }, UpdatedAt: now.Add(time.Minute), }, Res: &Alert{ Alert: model.Alert{ StartsAt: now.Add(-time.Minute), EndsAt: now.Add(2 * time.Minute), }, UpdatedAt: now.Add(time.Minute), }, }, { // Alert A has the Timeout flag unset while Alert B has it set. // StartsAt is defined by Alert A. // EndsAt is defined by Alert A. A: &Alert{ Alert: model.Alert{ StartsAt: now.Add(-time.Minute), EndsAt: now.Add(3 * time.Minute), }, UpdatedAt: now, }, B: &Alert{ Alert: model.Alert{ StartsAt: now, EndsAt: now.Add(2 * time.Minute), }, UpdatedAt: now.Add(time.Minute), Timeout: true, }, Res: &Alert{ Alert: model.Alert{ StartsAt: now.Add(-time.Minute), EndsAt: now.Add(3 * time.Minute), }, UpdatedAt: now.Add(time.Minute), Timeout: true, }, }, { // Both alerts have the Timeout flag unset and are not resolved. // StartsAt is defined by Alert A. // EndsAt is defined by Alert A. A: &Alert{ Alert: model.Alert{ StartsAt: now.Add(-time.Minute), EndsAt: now.Add(3 * time.Minute), }, UpdatedAt: now, }, B: &Alert{ Alert: model.Alert{ StartsAt: now, EndsAt: now.Add(2 * time.Minute), }, UpdatedAt: now.Add(time.Minute), }, Res: &Alert{ Alert: model.Alert{ StartsAt: now.Add(-time.Minute), EndsAt: now.Add(3 * time.Minute), }, UpdatedAt: now.Add(time.Minute), }, }, { // Both alerts have the Timeout flag unset and are not resolved. // StartsAt is defined by Alert A. // EndsAt is defined by Alert B. A: &Alert{ Alert: model.Alert{ StartsAt: now.Add(-time.Minute), EndsAt: now.Add(3 * time.Minute), }, UpdatedAt: now, }, B: &Alert{ Alert: model.Alert{ StartsAt: now.Add(-time.Minute), EndsAt: now.Add(4 * time.Minute), }, UpdatedAt: now.Add(time.Minute), }, Res: &Alert{ Alert: model.Alert{ StartsAt: now.Add(-time.Minute), EndsAt: now.Add(4 * time.Minute), }, UpdatedAt: now.Add(time.Minute), }, }, { // Both alerts have the Timeout flag unset, A is resolved while B isn't. // StartsAt is defined by Alert A. // EndsAt is defined by Alert B. A: &Alert{ Alert: model.Alert{ StartsAt: now.Add(-3 * time.Minute), EndsAt: now.Add(-time.Minute), }, UpdatedAt: now, }, B: &Alert{ Alert: model.Alert{ StartsAt: now.Add(-2 * time.Minute), EndsAt: now.Add(time.Minute), }, UpdatedAt: now.Add(time.Minute), }, Res: &Alert{ Alert: model.Alert{ StartsAt: now.Add(-3 * time.Minute), EndsAt: now.Add(time.Minute), }, UpdatedAt: now.Add(time.Minute), }, }, { // Both alerts have the Timeout flag unset, B is resolved while A isn't. // StartsAt is defined by Alert A. // EndsAt is defined by Alert B. A: &Alert{ Alert: model.Alert{ StartsAt: now.Add(-2 * time.Minute), EndsAt: now.Add(3 * time.Minute), }, UpdatedAt: now, }, B: &Alert{ Alert: model.Alert{ StartsAt: now.Add(-2 * time.Minute), EndsAt: now, }, UpdatedAt: now.Add(time.Minute), }, Res: &Alert{ Alert: model.Alert{ StartsAt: now.Add(-2 * time.Minute), EndsAt: now, }, UpdatedAt: now.Add(time.Minute), }, }, { // Both alerts are resolved (EndsAt < now). // StartsAt is defined by Alert B. // EndsAt is defined by Alert A. A: &Alert{ Alert: model.Alert{ StartsAt: now.Add(-3 * time.Minute), EndsAt: now.Add(-time.Minute), }, UpdatedAt: now.Add(-time.Minute), }, B: &Alert{ Alert: model.Alert{ StartsAt: now.Add(-4 * time.Minute), EndsAt: now.Add(-2 * time.Minute), }, UpdatedAt: now.Add(time.Minute), }, Res: &Alert{ Alert: model.Alert{ StartsAt: now.Add(-4 * time.Minute), EndsAt: now.Add(-1 * time.Minute), }, UpdatedAt: now.Add(time.Minute), }, }, } for i, p := range pairs { p := p t.Run(strconv.Itoa(i), func(t *testing.T) { if res := p.A.Merge(p.B); !reflect.DeepEqual(p.Res, res) { t.Errorf("unexpected merged alert %#v", res) } if res := p.B.Merge(p.A); !reflect.DeepEqual(p.Res, res) { t.Errorf("unexpected merged alert %#v", res) } }) } } func TestCalcSilenceState(t *testing.T) { var ( pastStartTime = time.Now() pastEndTime = time.Now() futureStartTime = time.Now().Add(time.Hour) futureEndTime = time.Now().Add(time.Hour) ) expected := CalcSilenceState(futureStartTime, futureEndTime) require.Equal(t, SilenceStatePending, expected) expected = CalcSilenceState(pastStartTime, futureEndTime) require.Equal(t, SilenceStateActive, expected) expected = CalcSilenceState(pastStartTime, pastEndTime) require.Equal(t, SilenceStateExpired, expected) } func TestSilenceExpired(t *testing.T) { now := time.Now() silence := Silence{StartsAt: now, EndsAt: now} require.True(t, silence.Expired()) silence = Silence{StartsAt: now.Add(time.Hour), EndsAt: now.Add(time.Hour)} require.True(t, silence.Expired()) silence = Silence{StartsAt: now, EndsAt: now.Add(time.Hour)} require.False(t, silence.Expired()) } func TestAlertSliceSort(t *testing.T) { var ( a1 = &Alert{ Alert: model.Alert{ Labels: model.LabelSet{ "job": "j1", "instance": "i1", "alertname": "an1", }, }, } a2 = &Alert{ Alert: model.Alert{ Labels: model.LabelSet{ "job": "j1", "instance": "i1", "alertname": "an2", }, }, } a3 = &Alert{ Alert: model.Alert{ Labels: model.LabelSet{ "job": "j2", "instance": "i1", "alertname": "an1", }, }, } a4 = &Alert{ Alert: model.Alert{ Labels: model.LabelSet{ "alertname": "an1", }, }, } a5 = &Alert{ Alert: model.Alert{ Labels: model.LabelSet{ "alertname": "an2", }, }, } ) cases := []struct { alerts AlertSlice exp AlertSlice }{ { alerts: AlertSlice{a2, a1}, exp: AlertSlice{a1, a2}, }, { alerts: AlertSlice{a3, a2, a1}, exp: AlertSlice{a1, a2, a3}, }, { alerts: AlertSlice{a4, a2, a4}, exp: AlertSlice{a2, a4, a4}, }, { alerts: AlertSlice{a5, a4}, exp: AlertSlice{a4, a5}, }, } for _, tc := range cases { sort.Stable(tc.alerts) if !reflect.DeepEqual(tc.alerts, tc.exp) { t.Fatalf("expected %v but got %v", tc.exp, tc.alerts) } } } prometheus-alertmanager-0.15.3+ds/ui/000077500000000000000000000000001341674552200174635ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/ui/Dockerfile000066400000000000000000000001461341674552200214560ustar00rootroot00000000000000FROM node:6.10 RUN npm install -g elm@0.18.0 elm-format@0.6.1-alpha elm-test@0.18.3 uglify-js@3.0.15 prometheus-alertmanager-0.15.3+ds/ui/app/000077500000000000000000000000001341674552200202435ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/ui/app/.gitignore000066400000000000000000000000331341674552200222270ustar00rootroot00000000000000dist/ elm-stuff/ script.js prometheus-alertmanager-0.15.3+ds/ui/app/CONTRIBUTING.md000066400000000000000000000050741341674552200225020ustar00rootroot00000000000000# Contributing This document describes how to: - Set up your dev environment - Become familiar with [Elm](http://elm-lang.org/) - Develop against AlertManager ## Dev Environment Setup You can either use our default Docker setup or install all dev dependencies locally. For the former you only need Docker installed, for the latter you need to set the environment flag `NO_DOCKER` to `true` and have the following dependencies installed: - [Elm](https://guide.elm-lang.org/install.html#install) - [Elm-Format](https://github.com/avh4/elm-format) is installed In addition for easier development you can [configure](https://guide.elm-lang.org/install.html#configure-your-editor) your editor. **All submitted elm code must be formatted with `elm-format`**. Install and execute it however works best for you. We recommend having formatting the file on save, similar to how many developers use `gofmt`. If you prefer, there's a make target available to format all elm source files: ``` # make format ``` Once you've installed Elm, install the dependencies listed in `elm-package.json`: ``` # elm package install -y ``` ## Elm Resources - The [Official Elm Guide](https://guide.elm-lang.org/) is a great place to start. Going through the entire guide takes about an hour, and is a good primer to get involved in our codebase. Once you've worked through it, you should be able to start writing your feature with the help of the compiler. - Check the [syntax reference](http://elm-lang.org/docs/syntax) when you need a reminder of how the language works. - Read up on [how to write elm code](http://elm-lang.org/docs/style-guide). - Watch videos from the latest [elm-conf](https://www.youtube.com/channel/UCOpGiN9AkczVjlpGDaBwQrQ) - Learn how to use the debugger! Elm comes packaged with an excellent [debugger](http://elm-lang.org/blog/the-perfect-bug-report). We've found this tool to be invaluable in understanding how the app is working as we're debugging behavior. ## Local development workflow At the top level of this repo, follow the HA AlertManager instructions. Compile the binary, then run with `goreman`. Add example alerts with the file provided in the HA example folder. Then start the development server: ``` # cd ui/app # make dev-server ``` Your app should be available at `http://localhost:`. Navigate to `src/Main.elm`. Any changes to the file system are detected automatically, triggering a recompile of the project. ## Commiting changes Before you commit changes, please run `make build-all` on the root level Makefile. Please include `ui/bindata.go` in your commit. prometheus-alertmanager-0.15.3+ds/ui/app/Makefile000066400000000000000000000021221341674552200217000ustar00rootroot00000000000000ELM_FILES := $(shell find src -iname *.elm) DOCKER_IMG :=elm-env DOCKER_CMD := docker run --rm -t -v $(PWD):/app -w /app $(DOCKER_IMG) # macOS requires mktemp template to be at the end of the filename. TEMPFILE := $(shell mktemp ./elm-XXXXXXXXXX) # --output flag for elm make must end in .js or .html. TEMPFILE_JS := "$(TEMPFILE).js" ifeq ($(NO_DOCKER), true) DOCKER_CMD= endif all: test script.js elm-env: @(if [ "$(NO_DOCKER)" != "true" ] ; then \ echo ">> building elm-env docker image"; \ docker build -t $(DOCKER_IMG) ../. > /dev/null; \ fi; ) format: elm-env $(ELM_FILES) @echo ">> format front-end code" @$(DOCKER_CMD) elm-format --yes $(ELM_FILES) test: elm-env @$(DOCKER_CMD) elm-format $(ELM_FILES) --validate @$(DOCKER_CMD) elm-test dev-server: elm-reactor script.js: elm-env format $(ELM_FILES) @echo ">> building script.js" @$(DOCKER_CMD) elm make src/Main.elm --yes --output $(TEMPFILE_JS) @$(DOCKER_CMD) uglifyjs $(TEMPFILE_JS) --compress unused --mangle --output $(@) @rm -rf $(TEMPFILE_JS) @rm -rf $(TEMPFILE) clean: - @rm script.js - @docker rmi $(DOCKER_IMG) prometheus-alertmanager-0.15.3+ds/ui/app/README.md000066400000000000000000000027261341674552200215310ustar00rootroot00000000000000# Alertmanager UI This is a re-write of the Alertmanager UI in [elm-lang](http://elm-lang.org/). ## Usage ### Filtering on the alerts page By default, the alerts page only shows active (not silenced) alerts. Adding a query string containing the following will additionally show silenced alerts. ``` http://alertmanager/#/alerts?silenced=true ``` The alerts page can also be filtered by the receivers for a page. Receivers are configured in Alertmanager's yaml configuration file. ``` http://alertmanager/#/alerts?receiver=backend ``` Filtering based on label matchers is available. They can easily be added and modified through the UI. ``` http://alertmanager/#/alerts?filter=%7Bseverity%3D%22warning%22%2C%20owner%3D%22backend%22%7D ``` These filters can be used in conjunction. ### Filtering on the silences page Filtering based on label matchers is available. They can easily be added and modified through the UI. ``` http://alertmanager/#/silences?filter=%7Bseverity%3D%22warning%22%2C%20owner%3D%22backend%22%7D ``` ### Note on filtering via label matchers Filtering via label matchers follows the same syntax and semantics as Prometheus. A properly formatted filter is a set of label matchers joined by accepted matching operators, surrounded by curly braces: ``` {foo="bar", baz=~"quu.*"} ``` Operators include: - `=` - `!=` - `=~` - `!~` See the official documentation for additional information: https://prometheus.io/docs/querying/basics/#instant-vector-selectors prometheus-alertmanager-0.15.3+ds/ui/app/elm-package.json000066400000000000000000000014521341674552200233060ustar00rootroot00000000000000{ "version": "1.0.0", "summary": "Alertmanager UI", "repository": "https://github.com/prometheus/alertmanager.git", "license": "Apache-2.0", "source-directories": [ "src" ], "exposed-modules": [], "dependencies": { "elm-lang/core": "5.0.0 <= v < 6.0.0", "elm-lang/dom": "1.1.1 <= v < 2.0.0", "elm-lang/html": "2.0.0 <= v < 3.0.0", "elm-lang/http": "1.0.0 <= v < 2.0.0", "elm-lang/navigation": "2.0.1 <= v < 3.0.0", "elm-tools/parser": "2.0.0 <= v < 3.0.0", "evancz/url-parser": "2.0.1 <= v < 3.0.0", "jweir/elm-iso8601": "3.0.2 <= v < 4.0.0", "krisajenkins/elm-dialog": "4.0.3 <= v < 5.0.0", "rluiten/elm-date-extra": "8.5.0 <= v < 9.0.0" }, "elm-version": "0.18.0 <= v < 0.19.0" } prometheus-alertmanager-0.15.3+ds/ui/app/favicon.ico000066400000000000000000000353561341674552200224000ustar00rootroot00000000000000 h6  ¨ž00 ¨%F(  ÿÿÿÿÿÿIÿÿÿ¨ýýÿäìïýûíðýûýýÿâÿÿÿ¤ÿÿÿDÿÿÿÿÿÿÿÿÿ”ÿÿÿï÷ùþÿš¬óÿPoêÿQqêÿ ±ôÿùúþÿÿÿÿíÿÿÿÿÿÿÿÿÿÿÿÿ²ÿÿÿÿÿÿÿÿØßúÿ]zìÿRqêÿRqêÿ`}ìÿÞäûÿÿÿÿÿÿÿÿþÿÿÿªÿÿÿÿÿÿÿÿÿ“ÿÿÿÿüýÿÿ³Áöÿ¤òÿ‡ñÿ‡ñÿ‡ñÿ‡ñÿ¤òÿ·Äöÿýþÿÿÿÿÿþÿÿÿ‰ÿÿÿÿÿÿJÿÿÿîÿÿÿÿúûþÿƒšðÿTrëÿWuëÿWuëÿWuëÿWuëÿSrëÿŠŸñÿüýÿÿÿÿÿÿÿÿÿéÿÿÿ@ÿÿÿ¨ÿÿÿÿÿÿÿÿóõþÿ§·ôÿ•¨òÿ—ªóÿ—ªóÿ—ªóÿ—ªóÿ•¨òÿ©¹õÿõöþÿÿÿÿÿÿÿÿÿÿÿÿ›ÿÿÿãÿÿÿÿÿÿÿÿ¡²ôÿ4Xçÿ0Uæÿ+Qæÿ+Qæÿ+Qæÿ+Qæÿ2Wçÿ3Xçÿ¦¶ôÿÿÿÿÿÿÿÿÿÿÿÿÙÿÿÿûÿÿÿÿúûþÿ¶Ãöÿ¯½õÿNnêÿ*Qæÿ,Ræÿ,Ræÿ*Pæÿhƒíÿ³Áöÿ¶ÃöÿûüÿÿÿÿÿÿÿÿÿòÿÿÿûÿÿÿÿÿÿÿÿÿÿÿÿËÕùÿ5Zçÿ+Ræÿ,Ræÿ,Ræÿ*PæÿLlêÿêîüÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿòÿÿÿâÿÿÿÿÿÿÿÿÿÿÿÿÅÏøÿ1Wçÿ+Ræÿ,Ræÿ,Ræÿ*QæÿDféÿåéüÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿØÿÿÿ¦ÿÿÿÿÿÿÿÿÿÿÿÿãèüÿGhéÿ-Sæÿ+Qæÿ8\çÿ1Vçÿb~ìÿö÷þÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ™ÿÿÿGÿÿÿìÿÿÿÿÿÿÿÿüýÿÿz’ïÿd€íÿ2WçÿTsëÿLlêÿ®½õÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿçÿÿÿ>ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ¶Ãöÿ·ÄöÿCeéÿˆžñÿ£òÿåêüÿÿÿÿÿÿÿÿÿÿÿÿþÿÿÿ…ÿÿÿÿÿÿÿÿÿ­ÿÿÿþÿÿÿÿõ÷þÿëîýÿ^{ìÿÌÕùÿáæûÿö÷þÿÿÿÿÿÿÿÿýÿÿÿ¥ÿÿÿÿÿÿÿÿÿŽÿÿÿíÿÿÿÿûüÿÿ­¼õÿíðýÿÿÿÿÿÿÿÿÿÿÿÿêÿÿÿ‡ÿÿÿÿÿÿÿÿÿDÿÿÿ¢ÿÿÿÞùúþöüýÿõÿÿÿÜÿÿÿÿÿÿ@ÿÿÿøàÀ€€€€Ààø( @ ÿÿÿÿÿÿ*ÿÿÿnÿÿÿ®ÿÿÿÚÿÿÿòÿÿÿüÿÿÿüÿÿÿðÿÿÿ×ÿÿÿ¨ÿÿÿgÿÿÿ$ÿÿÿÿÿÿÿÿÿ8ÿÿÿ˜ÿÿÿàÿÿÿüÿÿÿÿÿÿÿÿùúþÿîñýÿïòýÿûûþÿÿÿÿÿÿÿÿÿÿÿÿúÿÿÿÛÿÿÿÿÿÿ0ÿÿÿÿÿÿÿÿÿÿÿÿëÿÿÿÿÿÿÿÿÿÿÿÿöøþÿ²Àöÿk†íÿPoêÿQpêÿqŠîÿ»Ç÷ÿùúþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿåÿÿÿ€ÿÿÿÿÿÿ8ÿÿÿÅÿÿÿþÿÿÿÿÿÿÿÿÿÿÿÿ÷ùþÿˆžñÿ0Uæÿ(Oæÿ*Pæÿ*Pæÿ(Oæÿ4Xçÿ—ªóÿûüÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿýÿÿÿ¸ÿÿÿ,ÿÿÿDÿÿÿÛÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿºÆ÷ÿ.Tæÿ(Oæÿ)Oæÿ)Oæÿ)Oæÿ)Oæÿ'Nåÿ5YçÿÊÓùÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÐÿÿÿ6ÿÿÿ8ÿÿÿÛÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ«ºõÿ_|ìÿb~ìÿb~ìÿb~ìÿb~ìÿb~ìÿb~ìÿ`}ìÿ¹Æ÷ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÎÿÿÿ+ÿÿÿÿÿÿÄÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿóöþÿéíüÿêîüÿåêüÿâçüÿâçüÿâçüÿâçüÿâçüÿâçüÿâçüÿâçüÿæêüÿêîüÿêíüÿõ÷þÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ³ÿÿÿÿÿÿÿÿÿŒÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ¯óÿGhéÿJkêÿKkêÿKkêÿKkêÿKkêÿKkêÿKkêÿKkêÿKkêÿKkêÿKkêÿJkêÿIiéÿ­¼õÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿýÿÿÿwÿÿÿ:ÿÿÿêÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿŠŸñÿ#Kåÿ'Nåÿ'Nåÿ'Nåÿ'Nåÿ'Nåÿ'Nåÿ'Nåÿ'Nåÿ'Nåÿ'Nåÿ'Nåÿ'Nåÿ%Låÿ¯óÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿßÿÿÿ*ÿÿÿÿÿÿ™ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ­¼õÿf‚íÿhƒíÿhƒíÿhƒíÿhƒíÿhƒíÿhƒíÿhƒíÿhƒíÿhƒíÿhƒíÿhƒíÿhƒíÿg‚íÿ»Ç÷ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ„ÿÿÿ+ÿÿÿßÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿöøþÿïòýÿïòýÿïòýÿïòýÿïòýÿïòýÿïòýÿïòýÿïòýÿïòýÿïòýÿïòýÿïòýÿïòýÿ÷ùþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÑÿÿÿÿÿÿnÿÿÿûÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿíñýÿ|”ïÿYwëÿ[xëÿ[xëÿ[xëÿ[xëÿ[xëÿ[xëÿ[xëÿ[xëÿ[xëÿ[xëÿ[xëÿ[xëÿYwëÿ…›ðÿòôýÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿöÿÿÿVÿÿÿ®ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿýýÿÿ£òÿ+Qæÿ)Oæÿ)Oæÿ)Pæÿ)Pæÿ)Pæÿ)Pæÿ)Pæÿ)Pæÿ)Pæÿ)Pæÿ)Pæÿ)Pæÿ)Pæÿ)Oæÿ,Ræÿ™¬óÿþþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ”ÿÿÿÙÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÊÔùÿ8\çÿ'Nåÿ+Qæÿ1Vçÿ-Sæÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ/Uæÿ/Tæÿ)Pæÿ'Nåÿ?aèÿÕÝúÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÃÿÿÿòÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿýþÿÿŒ ñÿRqëÿy‘ïÿ£´ôÿ}”ïÿ-Sæÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ+QæÿDeéÿ¢³ôÿ’¦òÿl‡îÿMmêÿ˜ªóÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÞÿÿÿýÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿýýÿÿåêüÿðóýÿþþÿÿëïýÿWuëÿ*Pæÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ-SæÿŸ°ôÿÿÿÿÿûüþÿìðýÿåêüÿþþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿëÿÿÿüÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ¸Å÷ÿ/Tæÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ)Pæÿ\yìÿóõþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿëÿÿÿñÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ”§òÿ)Pæÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ+QæÿCeéÿâçüÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÝÿÿÿ×ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿŽ¢òÿ)Oæÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ+Qæÿ?bèÿÞäûÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÀÿÿÿªÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ¥µôÿ+Qæÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ*PæÿHiéÿèìüÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿjÿÿÿûÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÍÖùÿ6[çÿ+Qæÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ+Ræÿ9\çÿ2Wçÿ,Ræÿ)OæÿeíÿøùþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿõÿÿÿRÿÿÿ(ÿÿÿÜÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿñôýÿZxëÿ)Pæÿ/Uæÿ.Tæÿ,Ræÿ,Ræÿ*QæÿVtëÿ?aèÿ+Qæÿ,Ræÿ¢²ôÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÎÿÿÿÿÿÿÿÿÿ“ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ–©òÿ(OåÿTsëÿ]zìÿ*Pæÿ,Ræÿ*PæÿsŒîÿMmêÿ(OæÿPoêÿæêüÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ~ÿÿÿ5ÿÿÿçÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÏØùÿ5YçÿvŽïÿ§·õÿ-Ræÿ,Ræÿ*Qæÿ•¨òÿd€íÿ*Pæÿ¢³ôÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÛÿÿÿ%ÿÿÿÿÿÿ…ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿïòýÿQpêÿŸ±ôÿØßúÿ:^èÿ+Qæÿ3XçÿÅÏøÿ…›ðÿAdéÿáæûÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿûÿÿÿpÿÿÿÿÿÿ½ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿóõþÿ{“ïÿÝãûÿìïýÿKlêÿ(NåÿUtëÿñôýÿµÃöÿg‚íÿ÷ùþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ«ÿÿÿÿÿÿ1ÿÿÿÔÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿøùþÿàæûÿþÿÿÿòõýÿVuëÿ'Nåÿ”§òÿÿÿÿÿêíüÿ™¬óÿøùþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÇÿÿÿ$ÿÿÿ<ÿÿÿÔÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿøúþÿd€íÿ8\çÿÔÛúÿÿÿÿÿÿÿÿÿçëüÿùúþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÈÿÿÿ/ÿÿÿ0ÿÿÿ»ÿÿÿýÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ˜ªóÿYwëÿóöþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿûÿÿÿ®ÿÿÿ%ÿÿÿÿÿÿ€ÿÿÿåÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿæêüÿ”¨òÿóõþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿßÿÿÿsÿÿÿÿÿÿÿÿÿ0ÿÿÿŽÿÿÿÙÿÿÿùÿÿÿÿÿÿÿÿÿÿÿÿõ÷þÿûüÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ÷ÿÿÿÔÿÿÿ…ÿÿÿ(ÿÿÿÿÿÿÿÿÿ"ÿÿÿ`ÿÿÿžÿÿÿÌÿÿÿçÿÿÿòÿÿÿñÿÿÿåÿÿÿÈÿÿÿ˜ÿÿÿYÿÿÿÿÿÿÿðÿÿ€ÿþü?øðàÀÀ€€€€€€ÀÀàðøü?þÿÿ€ÿÿðÿ(0` $ÿÿÿÿÿÿ9ÿÿÿoÿÿÿ¦ÿÿÿÍÿÿÿæÿÿÿöÿÿÿüÿÿÿüÿÿÿôÿÿÿãÿÿÿÈÿÿÿžÿÿÿfÿÿÿ1ÿÿÿ ÿÿÿÿÿÿ)ÿÿÿtÿÿÿ¾ÿÿÿëÿÿÿüÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿûÿÿÿæÿÿÿ´ÿÿÿhÿÿÿ ÿÿÿÿÿÿÿÿÿ0ÿÿÿ’ÿÿÿãÿÿÿþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿøùþÿïòýÿðóýÿùúþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿýÿÿÿÚÿÿÿƒÿÿÿ$ÿÿÿÿÿÿÿÿÿ‚ÿÿÿåÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿûüÿÿÐØùÿŽ¢ñÿcìÿRqêÿSrëÿhƒíÿ—©óÿÙàúÿýþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿþÿÿÿÚÿÿÿoÿÿÿÿÿÿÿÿÿ?ÿÿÿÃÿÿÿýÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿðóýÿŽ¢òÿ;^èÿ)Pæÿ)Oæÿ*Pæÿ*Pæÿ)Oæÿ*PæÿCdéÿ ±ôÿ÷øþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿûÿÿÿ±ÿÿÿ.ÿÿÿÿÿÿcÿÿÿåÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿöøþÿ€—ðÿ,Ræÿ+Qæÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ*Pæÿ0Uæÿ—ªóÿüýÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿØÿÿÿLÿÿÿÿÿÿÿÿÿvÿÿÿòÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ´Âöÿ1Vçÿ+Qæÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ+Qæÿ:^èÿÌÕùÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿèÿÿÿ]ÿÿÿÿÿÿÿÿÿvÿÿÿõÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿûüÿÿnˆîÿ%Låÿ(Oæÿ(Oæÿ(Oæÿ(Oæÿ(Oæÿ(Oæÿ(Oæÿ(Oæÿ(Oæÿ(Oæÿ%Låÿ‰žñÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿìÿÿÿ\ÿÿÿÿÿÿbÿÿÿòÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ÷øþÿ£òÿo‰îÿqŠîÿqŠîÿqŠîÿqŠîÿqŠîÿqŠîÿqŠîÿqŠîÿqŠîÿqŠîÿnˆîÿŸ°ôÿýýÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿæÿÿÿHÿÿÿ?ÿÿÿäÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÓÿÿÿ)ÿÿÿÿÿÿÁÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÞäûÿ¡²ôÿ ±ôÿ¡±ôÿ¡±ôÿ¡²ôÿ¡²ôÿ¡²ôÿ¡²ôÿ¡²ôÿ¡²ôÿ¡²ôÿ¡²ôÿ¡²ôÿ¡²ôÿ¡²ôÿ¡²ôÿ¡²ôÿ¡²ôÿ¡±ôÿ¡±ôÿ ±ôÿ¥µôÿéíüÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ§ÿÿÿ ÿÿÿÿÿÿ€ÿÿÿþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ¶Ãöÿ,Ræÿ*Pæÿ*Qæÿ*Qæÿ*Qæÿ*Qæÿ*Qæÿ*Qæÿ*Qæÿ*Qæÿ*Qæÿ*Qæÿ*Qæÿ*Qæÿ*Qæÿ*Qæÿ*Qæÿ*Qæÿ*Qæÿ*Qæÿ*Pæÿ5YçÿÎ×ùÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿøÿÿÿaÿÿÿ1ÿÿÿäÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ¶Ãöÿ.Tæÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ+Qæÿ6[çÿÏ×ùÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÏÿÿÿÿÿÿÿÿÿ”ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿµÂöÿ*Qæÿ(Oæÿ(Oæÿ(Oæÿ(Oæÿ(Oæÿ(Oæÿ(Oæÿ(Oæÿ(Oæÿ(Oæÿ(Oæÿ(Oæÿ(Oæÿ(Oæÿ(Oæÿ(Oæÿ(Oæÿ(Oæÿ(Oæÿ(Oåÿ3XçÿÎ×ùÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿþÿÿÿtÿÿÿ*ÿÿÿãÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÒÚúÿ~•ðÿ}”ðÿ}”ðÿ}”ðÿ}”ðÿ}”ðÿ}”ðÿ}”ðÿ}”ðÿ}”ðÿ}”ðÿ}”ðÿ}”ðÿ}”ðÿ}”ðÿ}”ðÿ}”ðÿ}”ðÿ}”ðÿ}”ðÿ|”ïÿƒšðÿáæûÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÍÿÿÿÿÿÿuÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿùÿÿÿUÿÿÿÿÿÿ¾ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿâçûÿÀË÷ÿÀÌ÷ÿÀÌ÷ÿÀÌ÷ÿÀÌ÷ÿÀÌ÷ÿÀÌ÷ÿÀÌ÷ÿÀÌ÷ÿÀÌ÷ÿÀÌ÷ÿÀÌ÷ÿÀÌ÷ÿÀÌ÷ÿÀÌ÷ÿÀÌ÷ÿÀÌ÷ÿÀÌ÷ÿÀÌ÷ÿÀÌ÷ÿÀË÷ÿÁÍøÿéíüÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ ÿÿÿÿÿÿ9ÿÿÿêÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿçëüÿ`|ìÿ/Tæÿ1Vçÿ1Vçÿ1Vçÿ1Vçÿ1Vçÿ1Vçÿ1Vçÿ1Vçÿ1Vçÿ1Vçÿ1Vçÿ1Vçÿ1Vçÿ1Vçÿ1Vçÿ1Vçÿ1Vçÿ1Vçÿ1Vçÿ0Vçÿ/Uæÿo‰îÿîñýÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ×ÿÿÿÿÿÿpÿÿÿüÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿøúþÿ€—ðÿ*Qæÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ+Ræÿ,RæÿŽ¢ñÿüüÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿôÿÿÿMÿÿÿ¥ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ´Âöÿ3Xçÿ+Qæÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ+Qæÿ9\çÿÄÎøÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿþÿÿÿ~ÿÿÿÍÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿëïýÿWuëÿ)Pæÿ+Qæÿ)Pæÿ)Oæÿ,Ræÿ.Tæÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ-Sæÿ.Tæÿ)Pæÿ)Oæÿ*Pæÿ+Qæÿ)Oæÿgƒíÿôöþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ¨ÿÿÿçÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿª¹õÿ+Qæÿ1VçÿBdéÿ_|ìÿ…›ðÿ˜ªóÿFgéÿ+Qæÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ2Wçÿ…›ðÿ”¨òÿl†íÿMmêÿ8\çÿ-Sæÿ1Vçÿ¿Ê÷ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÆÿÿÿöÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿýýÿÿ©¸õÿ—©óÿÀË÷ÿßåûÿõ÷þÿÿÿÿÿž¯óÿ/Tæÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ*PæÿgƒíÿòôýÿûüþÿêîýÿÒÚúÿ´ÂöÿŽ¢òÿ´ÂöÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÙÿÿÿýÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿýþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿâçûÿIjéÿ*Pæÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ0UæÿµÂöÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿýýÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿáÿÿÿýÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ©¹õÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ)OæÿsŒîÿüüÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿáÿÿÿõÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿþþÿÿy‘ïÿ(Oæÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ*PæÿNnêÿëïýÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿØÿÿÿåÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ÷ùþÿa}ìÿ)Oæÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ+Qæÿ>aèÿÝãûÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÄÿÿÿÊÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿõ÷þÿ]zìÿ)Pæÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ+Qæÿ<_èÿÙàúÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ¥ÿÿÿ¡ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿúûþÿi„íÿ)Oæÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ+Qæÿ@cèÿßåûÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿþÿÿÿzÿÿÿkÿÿÿûÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ†œñÿ)Oæÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ*PæÿNnêÿëïýÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿòÿÿÿIÿÿÿ4ÿÿÿçÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ²Àöÿ.Sæÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ8\çÿ0Væÿ,Ræÿ,Ræÿ,Ræÿ)Oæÿk†íÿúûþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÓÿÿÿÿÿÿ ÿÿÿ¸ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿßåûÿCeéÿ+Qæÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ+QæÿYwëÿ:]èÿ+Qæÿ,Ræÿ,Ræÿ+Qæÿ ±ôÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿšÿÿÿÿÿÿoÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿûüþÿsŒîÿ)Oæÿ,Ræÿ-Sæÿ3Xçÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ,RæÿwïÿBdéÿ+Qæÿ,Ræÿ*QæÿDeéÿÜãûÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ÷ÿÿÿOÿÿÿ%ÿÿÿÞÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ³Áöÿ/Tæÿ+Ræÿ4Xçÿ|”ïÿ5Zçÿ,Ræÿ,Ræÿ,Ræÿ,Ræÿ-Sæÿ”§òÿOnêÿ*Pæÿ,Ræÿ*Pæÿ‹ ñÿþþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÇÿÿÿÿÿÿÿÿÿ‹ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿåêüÿJjéÿ)Pæÿ9\çÿÄÏøÿ_|ìÿ)Pæÿ,Ræÿ,Ræÿ,Ræÿ0Uæÿ²Àöÿ`}ìÿ)Pæÿ*QæÿIiéÿÞäûÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿüÿÿÿkÿÿÿ*ÿÿÿÞÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿýýÿÿz’ïÿ'NåÿGhéÿèìüÿ˜ªóÿ*Pæÿ,Ræÿ,Ræÿ+Qæÿ8\çÿÐØùÿ{“ïÿ(Oæÿ+Qæÿ”§òÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÈÿÿÿÿÿÿvÿÿÿüÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ©¸õÿ)Oæÿi„íÿüüÿÿÅÏøÿ3Xçÿ,Ræÿ,Ræÿ*PæÿPoêÿîñýÿž°óÿ)Pæÿ=`èÿÖÝúÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿõÿÿÿXÿÿÿÿÿÿ¸ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÃÎøÿ1Vçÿ¥µôÿÿÿÿÿáæûÿBdéÿ+Qæÿ,Ræÿ)Oæÿƒ™ðÿÿÿÿÿÉÒøÿ2Wçÿ_|ìÿõ÷þÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿœÿÿÿÿÿÿ5ÿÿÿÝÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ¿Ë÷ÿ`}ìÿéíüÿÿÿÿÿðòýÿSrëÿ*Pæÿ+Ræÿ3XçÿÁÌøÿÿÿÿÿïòýÿSrëÿ{“ïÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÈÿÿÿ!ÿÿÿUÿÿÿíÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ×Þúÿ×Þúÿÿÿÿÿÿÿÿÿöøþÿ_|ìÿ)Pæÿ)PæÿZxëÿðóýÿÿÿÿÿÿÿÿÿ›­óÿ†œñÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÞÿÿÿ<ÿÿÿÿÿÿhÿÿÿñÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿúûþÿg‚íÿ)Oæÿ+Qæÿœ®óÿÿÿÿÿÿÿÿÿÿÿÿÿçìüÿ¦¶ôÿúûþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿåÿÿÿNÿÿÿÿÿÿgÿÿÿìÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ~•ðÿ'Nåÿ?aèÿØßúÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿðòýÿûüþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿßÿÿÿOÿÿÿÿÿÿSÿÿÿÜÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ´Âöÿ,Ræÿb~ìÿöøþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿ>ÿÿÿ2ÿÿÿ´ÿÿÿûÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿîñýÿ[yìÿq‹îÿýýÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿöÿÿÿ ÿÿÿ#ÿÿÿÿÿÿnÿÿÿÚÿÿÿþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÍÖùÿ†œñÿóõþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿýÿÿÿÍÿÿÿ\ÿÿÿ ÿÿÿÿÿÿ$ÿÿÿƒÿÿÿÙÿÿÿüÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿðóýÿúûþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿúÿÿÿÐÿÿÿsÿÿÿÿÿÿÿÿÿÿÿÿdÿÿÿ®ÿÿÿàÿÿÿøÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿþÿÿÿõÿÿÿÚÿÿÿ¤ÿÿÿYÿÿÿÿÿÿÿÿÿ)ÿÿÿZÿÿÿÿÿÿ·ÿÿÿÔÿÿÿäÿÿÿëÿÿÿëÿÿÿãÿÿÿÑÿÿÿ±ÿÿÿ†ÿÿÿRÿÿÿ"ÿÿÿÿÿàÿÿÿÿÿÿÿøÿÿàÿÿÀÿÿ€ÿÿÿþü?øðààÀÀÀ€€€€€€ÀÀÀàððøü?þÿÿÿ€ÿÿÀÿÿðÿÿø?ÿÿÿÿÿÿÿàÿÿprometheus-alertmanager-0.15.3+ds/ui/app/index.html000066400000000000000000000017611341674552200222450ustar00rootroot00000000000000 Alertmanager prometheus-alertmanager-0.15.3+ds/ui/app/src/000077500000000000000000000000001341674552200210325ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/ui/app/src/Alerts/000077500000000000000000000000001341674552200222645ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/ui/app/src/Alerts/Api.elm000066400000000000000000000031311341674552200234720ustar00rootroot00000000000000module Alerts.Api exposing (..) import Alerts.Types exposing (Alert, Receiver) import Json.Decode as Json exposing (..) import Utils.Api exposing (iso8601Time) import Utils.Filter exposing (Filter, generateQueryString) import Utils.Types exposing (ApiData) import Regex fetchReceivers : String -> Cmd (ApiData (List Receiver)) fetchReceivers apiUrl = Utils.Api.send (Utils.Api.get (apiUrl ++ "/receivers") (field "data" (list (Json.map (\receiver -> Receiver receiver (Regex.escape receiver)) string))) ) fetchAlerts : String -> Filter -> Cmd (ApiData (List Alert)) fetchAlerts apiUrl filter = let url = String.join "/" [ apiUrl, "alerts" ++ generateQueryString filter ] in Utils.Api.send (Utils.Api.get url alertsDecoder) alertsDecoder : Json.Decoder (List Alert) alertsDecoder = Json.list alertDecoder -- populate alerts with ids: |> Json.map (List.indexedMap (toString >> (|>))) |> field "data" {-| TODO: decode alert id when provided -} alertDecoder : Json.Decoder (String -> Alert) alertDecoder = Json.map6 Alert (Json.maybe (field "annotations" (Json.keyValuePairs Json.string)) |> andThen (Maybe.withDefault [] >> Json.succeed) ) (field "labels" (Json.keyValuePairs Json.string)) (Json.maybe (Json.at [ "status", "silencedBy", "0" ] Json.string)) (Json.maybe (Json.at [ "status", "inhibitedBy", "0" ] Json.string) |> Json.map ((/=) Nothing) ) (field "startsAt" iso8601Time) (field "generatorURL" Json.string) prometheus-alertmanager-0.15.3+ds/ui/app/src/Alerts/Types.elm000066400000000000000000000006001341674552200240630ustar00rootroot00000000000000module Alerts.Types exposing (Alert, Receiver) import Utils.Types exposing (Labels) import Time exposing (Time) type alias Alert = { annotations : Labels , labels : Labels , silenceId : Maybe String , isInhibited : Bool , startsAt : Time , generatorUrl : String , id : String } type alias Receiver = { name : String , regex : String } prometheus-alertmanager-0.15.3+ds/ui/app/src/Main.elm000066400000000000000000000064141341674552200224220ustar00rootroot00000000000000module Main exposing (main) import Navigation import Parsing import Views import Types exposing ( Route(..) , Msg ( NavigateToSilenceList , NavigateToSilenceView , NavigateToSilenceFormEdit , NavigateToSilenceFormNew , NavigateToAlerts , NavigateToNotFound , NavigateToStatus , RedirectAlerts ) , Model ) import Utils.Filter exposing (nullFilter) import Views.SilenceForm.Types exposing (initSilenceForm) import Views.Status.Types exposing (StatusModel, initStatusModel) import Views.AlertList.Types exposing (initAlertList) import Views.SilenceList.Types exposing (initSilenceList) import Views.SilenceView.Types exposing (initSilenceView) import Updates exposing (update) import Utils.Api as Api import Utils.Types exposing (ApiData(Loading)) import Json.Decode as Json main : Program Json.Value Model Msg main = Navigation.programWithFlags urlUpdate { init = init , update = update , view = Views.view , subscriptions = always Sub.none } init : Json.Value -> Navigation.Location -> ( Model, Cmd Msg ) init flags location = let route = Parsing.urlParser location filter = case route of AlertsRoute filter -> filter SilenceListRoute filter -> filter _ -> nullFilter prod = flags |> Json.decodeValue (Json.field "production" Json.bool) |> Result.withDefault False defaultCreator = flags |> Json.decodeValue (Json.field "defaultCreator" Json.string) |> Result.withDefault "" apiUrl = if prod then Api.makeApiUrl location.pathname else Api.makeApiUrl "http://localhost:9093/" libUrl = if prod then location.pathname else "/" in update (urlUpdate location) (Model initSilenceList initSilenceView initSilenceForm initAlertList route filter initStatusModel location.pathname apiUrl libUrl Loading Loading defaultCreator ) urlUpdate : Navigation.Location -> Msg urlUpdate location = let route = Parsing.urlParser location in case route of SilenceListRoute maybeFilter -> NavigateToSilenceList maybeFilter SilenceViewRoute silenceId -> NavigateToSilenceView silenceId SilenceFormEditRoute silenceId -> NavigateToSilenceFormEdit silenceId SilenceFormNewRoute matchers -> NavigateToSilenceFormNew matchers AlertsRoute filter -> NavigateToAlerts filter StatusRoute -> NavigateToStatus TopLevelRoute -> RedirectAlerts NotFoundRoute -> NavigateToNotFound prometheus-alertmanager-0.15.3+ds/ui/app/src/Parsing.elm000066400000000000000000000034601341674552200231370ustar00rootroot00000000000000module Parsing exposing (..) import Views.AlertList.Parsing exposing (alertsParser) import Views.SilenceList.Parsing exposing (silenceListParser) import Views.SilenceView.Parsing exposing (silenceViewParser) import Views.SilenceForm.Parsing exposing (silenceFormNewParser, silenceFormEditParser) import Views.Status.Parsing exposing (statusParser) import Navigation import UrlParser exposing ((), (), Parser, int, map, oneOf, parseHash, s, string, stringParam, top) import Regex import Types exposing (Route(..)) urlParser : Navigation.Location -> Route urlParser location = let -- Parse a query string occurring after the hash if it exists, and use -- it for routing. hashAndQuery = Regex.split (Regex.AtMost 1) (Regex.regex "\\?") (location.hash) hash = case List.head hashAndQuery of Just hash -> hash Nothing -> "" query = if List.length hashAndQuery == 2 then case List.head <| List.reverse hashAndQuery of Just query -> "?" ++ query Nothing -> "" else "" in case parseHash routeParser { location | search = query, hash = hash } of Just route -> route Nothing -> NotFoundRoute routeParser : Parser (Route -> a) a routeParser = oneOf [ map SilenceListRoute silenceListParser , map StatusRoute statusParser , map SilenceFormNewRoute silenceFormNewParser , map SilenceViewRoute silenceViewParser , map SilenceFormEditRoute silenceFormEditParser , map AlertsRoute alertsParser , map TopLevelRoute top ] prometheus-alertmanager-0.15.3+ds/ui/app/src/Silences/000077500000000000000000000000001341674552200225775ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/ui/app/src/Silences/Api.elm000066400000000000000000000035321341674552200240120ustar00rootroot00000000000000module Silences.Api exposing (..) import Http import Silences.Types exposing (Silence) import Utils.Types exposing (ApiData(..)) import Utils.Filter exposing (Filter) import Utils.Api import Silences.Decoders exposing (show, list, create, destroy) import Silences.Encoders import Utils.Filter exposing (generateQueryString) getSilences : String -> Filter -> (ApiData (List Silence) -> msg) -> Cmd msg getSilences apiUrl filter msg = let url = String.join "/" [ apiUrl, "silences" ++ (generateQueryString filter) ] in Utils.Api.send (Utils.Api.get url list) |> Cmd.map msg getSilence : String -> String -> (ApiData Silence -> msg) -> Cmd msg getSilence apiUrl uuid msg = let url = String.join "/" [ apiUrl, "silence", uuid ] in Utils.Api.send (Utils.Api.get url show) |> Cmd.map msg create : String -> Silence -> Cmd (ApiData String) create apiUrl silence = let url = String.join "/" [ apiUrl, "silences" ] body = Http.jsonBody <| Silences.Encoders.silence silence in -- TODO: This should return the silence, not just the ID, so that we can -- redirect to the silence show page. Utils.Api.send (Utils.Api.post url body Silences.Decoders.create) destroy : String -> Silence -> (ApiData String -> msg) -> Cmd msg destroy apiUrl silence msg = -- The incorrect route using "silences" receives a 405. The route seems to -- be matching on /silences and ignoring the :sid, should be getting a 404. let url = String.join "/" [ apiUrl, "silence", silence.id ] responseDecoder = -- Silences.Encoders.silence silence Silences.Decoders.destroy in Utils.Api.send (Utils.Api.delete url responseDecoder) |> Cmd.map msg prometheus-alertmanager-0.15.3+ds/ui/app/src/Silences/Decoders.elm000066400000000000000000000040131341674552200250240ustar00rootroot00000000000000module Silences.Decoders exposing (show, list, create, destroy) import Json.Decode as Json exposing (field, succeed, fail) import Utils.Api exposing (iso8601Time, (|:)) import Silences.Types exposing (Silence, Status, State(Active, Pending, Expired)) import Utils.Types exposing (Matcher, Time, ApiData(Initial)) show : Json.Decoder Silence show = Json.at [ "data" ] silenceDecoder list : Json.Decoder (List Silence) list = Json.at [ "data" ] (Json.list silenceDecoder) create : Json.Decoder String create = Json.at [ "data", "silenceId" ] Json.string destroy : Json.Decoder String destroy = Json.at [ "status" ] Json.string silenceDecoder : Json.Decoder Silence silenceDecoder = Json.succeed Silence |: (field "id" Json.string) |: (field "createdBy" Json.string) -- Remove this maybe once the api either disallows empty comments on -- creation, or returns an empty string. |: ((Json.maybe (field "comment" Json.string)) |> Json.andThen (\x -> Json.succeed <| Maybe.withDefault "" x) ) |: (field "startsAt" iso8601Time) |: (field "endsAt" iso8601Time) |: (field "updatedAt" iso8601Time) |: (field "matchers" (Json.list matcherDecoder)) |: (field "status" statusDecoder) statusDecoder : Json.Decoder Status statusDecoder = Json.succeed Status |: (field "state" Json.string |> Json.andThen stateDecoder) stateDecoder : String -> Json.Decoder State stateDecoder state = case state of "active" -> succeed Active "pending" -> succeed Pending "expired" -> succeed Expired _ -> fail <| "Silence.status.state must be one of 'active', 'pending' or 'expired' but was'" ++ state ++ "'." matcherDecoder : Json.Decoder Matcher matcherDecoder = Json.map3 Matcher (field "isRegex" Json.bool) (field "name" Json.string) (field "value" Json.string) prometheus-alertmanager-0.15.3+ds/ui/app/src/Silences/Encoders.elm000066400000000000000000000015121341674552200250370ustar00rootroot00000000000000module Silences.Encoders exposing (..) import Json.Encode as Encode import Silences.Types exposing (Silence) import Utils.Types exposing (Matcher) import Utils.Date silence : Silence -> Encode.Value silence silence = Encode.object [ ( "id", Encode.string silence.id ) , ( "createdBy", Encode.string silence.createdBy ) , ( "comment", Encode.string silence.comment ) , ( "startsAt", Encode.string (Utils.Date.encode silence.startsAt) ) , ( "endsAt", Encode.string (Utils.Date.encode silence.endsAt) ) , ( "matchers", Encode.list (List.map matcher silence.matchers) ) ] matcher : Matcher -> Encode.Value matcher m = Encode.object [ ( "name", Encode.string m.name ) , ( "value", Encode.string m.value ) , ( "isRegex", Encode.bool m.isRegex ) ] prometheus-alertmanager-0.15.3+ds/ui/app/src/Silences/Types.elm000066400000000000000000000023641341674552200244070ustar00rootroot00000000000000module Silences.Types exposing ( Silence , SilenceId , Status , State(Active, Pending, Expired) , nullSilence , nullSilenceStatus , nullMatcher , nullTime , stateToString ) import Utils.Types exposing (Matcher) import Time exposing (Time) nullSilence : Silence nullSilence = { id = "" , createdBy = "" , comment = "" , startsAt = 0 , endsAt = 0 , updatedAt = 0 , matchers = [ nullMatcher ] , status = nullSilenceStatus } nullSilenceStatus : Status nullSilenceStatus = { state = Expired } nullMatcher : Matcher nullMatcher = Matcher False "" "" nullTime : Time nullTime = 0 type alias Silence = { id : SilenceId , createdBy : String , comment : String , startsAt : Time , endsAt : Time , updatedAt : Time , matchers : List Matcher , status : Status } type alias Status = { state : State } type State = Active | Pending | Expired stateToString : State -> String stateToString state = case state of Active -> "active" Pending -> "pending" Expired -> "expired" type alias SilenceId = String prometheus-alertmanager-0.15.3+ds/ui/app/src/Status/000077500000000000000000000000001341674552200223155ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/ui/app/src/Status/Api.elm000066400000000000000000000030631341674552200235270ustar00rootroot00000000000000module Status.Api exposing (getStatus) import Utils.Api exposing (send, get) import Utils.Types exposing (ApiData) import Status.Types exposing (StatusResponse, VersionInfo, ClusterStatus, ClusterPeer) import Json.Decode exposing (Decoder, map2, string, field, at, list, int, maybe, bool) getStatus : String -> (ApiData StatusResponse -> msg) -> Cmd msg getStatus apiUrl msg = let url = String.join "/" [ apiUrl, "status" ] request = get url decodeStatusResponse in Cmd.map msg <| send request decodeStatusResponse : Decoder StatusResponse decodeStatusResponse = field "data" decodeData decodeData : Decoder StatusResponse decodeData = Json.Decode.map4 StatusResponse (field "configYAML" string) (field "uptime" string) (field "versionInfo" decodeVersionInfo) (field "clusterStatus" (maybe decodeClusterStatus)) decodeVersionInfo : Decoder VersionInfo decodeVersionInfo = Json.Decode.map6 VersionInfo (field "branch" string) (field "buildDate" string) (field "buildUser" string) (field "goVersion" string) (field "revision" string) (field "version" string) decodeClusterStatus : Decoder ClusterStatus decodeClusterStatus = Json.Decode.map3 ClusterStatus (field "name" string) (field "status" string) (field "peers" (list decodeClusterPeer)) decodeClusterPeer : Decoder ClusterPeer decodeClusterPeer = Json.Decode.map2 ClusterPeer (field "name" string) (field "address" string) prometheus-alertmanager-0.15.3+ds/ui/app/src/Status/Types.elm000066400000000000000000000011321341674552200241150ustar00rootroot00000000000000module Status.Types exposing (StatusResponse, VersionInfo, ClusterStatus, ClusterPeer) type alias StatusResponse = { config : String , uptime : String , versionInfo : VersionInfo , clusterStatus : Maybe ClusterStatus } type alias VersionInfo = { branch : String , buildDate : String , buildUser : String , goVersion : String , revision : String , version : String } type alias ClusterStatus = { name : String , status : String , peers : List ClusterPeer } type alias ClusterPeer = { name : String , address : String } prometheus-alertmanager-0.15.3+ds/ui/app/src/Types.elm000066400000000000000000000032751341674552200226440ustar00rootroot00000000000000module Types exposing (Model, Msg(..), Route(..)) import Views.AlertList.Types as AlertList exposing (AlertListMsg) import Views.SilenceList.Types as SilenceList exposing (SilenceListMsg) import Views.SilenceView.Types as SilenceView exposing (SilenceViewMsg) import Views.SilenceForm.Types as SilenceForm exposing (SilenceFormMsg) import Views.Status.Types exposing (StatusModel, StatusMsg) import Utils.Filter exposing (Filter, Matcher) import Utils.Types exposing (ApiData) type alias Model = { silenceList : SilenceList.Model , silenceView : SilenceView.Model , silenceForm : SilenceForm.Model , alertList : AlertList.Model , route : Route , filter : Filter , status : StatusModel , basePath : String , apiUrl : String , libUrl : String , bootstrapCSS : ApiData String , fontAwesomeCSS : ApiData String , defaultCreator : String } type Msg = MsgForAlertList AlertListMsg | MsgForSilenceView SilenceViewMsg | MsgForSilenceForm SilenceFormMsg | MsgForSilenceList SilenceListMsg | MsgForStatus StatusMsg | NavigateToAlerts Filter | NavigateToNotFound | NavigateToSilenceView String | NavigateToSilenceFormEdit String | NavigateToSilenceFormNew (List Matcher) | NavigateToSilenceList Filter | NavigateToStatus | Noop | RedirectAlerts | UpdateFilter String | BootstrapCSSLoaded (ApiData String) | FontAwesomeCSSLoaded (ApiData String) | SetDefaultCreator String type Route = AlertsRoute Filter | NotFoundRoute | SilenceFormEditRoute String | SilenceFormNewRoute (List Matcher) | SilenceListRoute Filter | SilenceViewRoute String | StatusRoute | TopLevelRoute prometheus-alertmanager-0.15.3+ds/ui/app/src/Updates.elm000066400000000000000000000110521341674552200231350ustar00rootroot00000000000000module Updates exposing (update) import Navigation import String exposing (trim) import Task import Types exposing ( Model , Msg(..) , Route(AlertsRoute, NotFoundRoute, SilenceFormEditRoute, SilenceFormNewRoute, SilenceListRoute, SilenceViewRoute, StatusRoute) ) import Views.AlertList.Types exposing (AlertListMsg(FetchAlerts)) import Views.AlertList.Updates import Views.SilenceForm.Types exposing (SilenceFormMsg(FetchSilence, NewSilenceFromMatchers)) import Views.SilenceForm.Updates import Views.SilenceList.Types exposing (SilenceListMsg(FetchSilences)) import Views.SilenceList.Updates import Views.SilenceView.Types exposing (SilenceViewMsg(InitSilenceView, SilenceFetched)) import Views.SilenceView.Updates import Views.Status.Types exposing (StatusMsg(InitStatusView)) import Views.Status.Updates update : Msg -> Model -> ( Model, Cmd Msg ) update msg ({ basePath, apiUrl } as model) = case msg of NavigateToAlerts filter -> let ( alertList, cmd ) = Views.AlertList.Updates.update FetchAlerts model.alertList filter apiUrl basePath in ( { model | alertList = alertList, route = AlertsRoute filter, filter = filter }, cmd ) NavigateToSilenceList filter -> let ( silenceList, cmd ) = Views.SilenceList.Updates.update FetchSilences model.silenceList filter basePath apiUrl in ( { model | silenceList = silenceList, route = SilenceListRoute filter, filter = filter } , Cmd.map MsgForSilenceList cmd ) NavigateToStatus -> ( { model | route = StatusRoute }, Task.perform identity (Task.succeed <| MsgForStatus InitStatusView) ) NavigateToSilenceView silenceId -> let ( silenceView, cmd ) = Views.SilenceView.Updates.update (InitSilenceView silenceId) model.silenceView apiUrl in ( { model | route = SilenceViewRoute silenceId, silenceView = silenceView } , Cmd.map MsgForSilenceView cmd ) NavigateToSilenceFormNew matchers -> ( { model | route = SilenceFormNewRoute matchers } , Task.perform (NewSilenceFromMatchers model.defaultCreator >> MsgForSilenceForm) (Task.succeed matchers) ) NavigateToSilenceFormEdit uuid -> ( { model | route = SilenceFormEditRoute uuid }, Task.perform identity (Task.succeed <| (FetchSilence uuid |> MsgForSilenceForm)) ) NavigateToNotFound -> ( { model | route = NotFoundRoute }, Cmd.none ) RedirectAlerts -> ( model, Navigation.newUrl (basePath ++ "#/alerts") ) UpdateFilter text -> let t = if trim text == "" then Nothing else Just text prevFilter = model.filter in ( { model | filter = { prevFilter | text = t } }, Cmd.none ) Noop -> ( model, Cmd.none ) MsgForStatus msg -> Views.Status.Updates.update msg model apiUrl MsgForAlertList msg -> let ( alertList, cmd ) = Views.AlertList.Updates.update msg model.alertList model.filter apiUrl basePath in ( { model | alertList = alertList }, cmd ) MsgForSilenceList msg -> let ( silenceList, cmd ) = Views.SilenceList.Updates.update msg model.silenceList model.filter basePath apiUrl in ( { model | silenceList = silenceList }, Cmd.map MsgForSilenceList cmd ) MsgForSilenceView msg -> let ( silenceView, cmd ) = Views.SilenceView.Updates.update msg model.silenceView apiUrl in ( { model | silenceView = silenceView }, Cmd.map MsgForSilenceView cmd ) MsgForSilenceForm msg -> let ( silenceForm, cmd ) = Views.SilenceForm.Updates.update msg model.silenceForm basePath apiUrl in ( { model | silenceForm = silenceForm }, cmd ) BootstrapCSSLoaded css -> ( { model | bootstrapCSS = css }, Cmd.none ) FontAwesomeCSSLoaded css -> ( { model | fontAwesomeCSS = css }, Cmd.none ) SetDefaultCreator name -> ( { model | defaultCreator = name }, Cmd.none ) prometheus-alertmanager-0.15.3+ds/ui/app/src/Utils/000077500000000000000000000000001341674552200221325ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/ui/app/src/Utils/Api.elm000066400000000000000000000060061341674552200233440ustar00rootroot00000000000000module Utils.Api exposing (..) import Http exposing (Error(..)) import Json.Decode as Json exposing (field) import Time exposing (Time) import Utils.Date import Utils.Types exposing (ApiData(..)) map : (a -> b) -> ApiData a -> ApiData b map fn response = case response of Success value -> Success (fn value) Initial -> Initial Loading -> Loading Failure a -> Failure a withDefault : a -> ApiData a -> a withDefault default response = case response of Success value -> value _ -> default parseError : String -> Maybe String parseError = Json.decodeString (field "error" Json.string) >> Result.toMaybe errorToString : Http.Error -> String errorToString err = case err of Timeout -> "Timeout exceeded" NetworkError -> "Network error" BadStatus resp -> parseError resp.body |> Maybe.withDefault (toString resp.status.code ++ " " ++ resp.status.message) BadPayload err resp -> -- OK status, unexpected payload "Unexpected response from api: " ++ err BadUrl url -> "Malformed url: " ++ url fromResult : Result Http.Error a -> ApiData a fromResult result = case result of Err e -> Failure (errorToString e) Ok x -> Success x send : Http.Request a -> Cmd (ApiData a) send = Http.send fromResult get : String -> Json.Decoder a -> Http.Request a get url decoder = request "GET" [] url Http.emptyBody decoder post : String -> Http.Body -> Json.Decoder a -> Http.Request a post url body decoder = request "POST" [] url body decoder delete : String -> Json.Decoder a -> Http.Request a delete url decoder = request "DELETE" [] url Http.emptyBody decoder request : String -> List Http.Header -> String -> Http.Body -> Json.Decoder a -> Http.Request a request method headers url body decoder = Http.request { method = method , headers = headers , url = url , body = body , expect = Http.expectJson decoder , timeout = Nothing , withCredentials = False } iso8601Time : Json.Decoder Time iso8601Time = Json.andThen (\strTime -> case Utils.Date.timeFromString strTime of Ok time -> Json.succeed time Err err -> Json.fail ("Could not decode time " ++ strTime ++ ": " ++ err) ) Json.string makeApiUrl : String -> String makeApiUrl externalUrl = let url = if String.endsWith "/" externalUrl then String.dropRight 1 externalUrl else externalUrl in url ++ "/api/v1" defaultTimeout : Time.Time defaultTimeout = 1000 * Time.millisecond (|:) : Json.Decoder (a -> b) -> Json.Decoder a -> Json.Decoder b (|:) = -- Taken from elm-community/json-extra flip (Json.map2 (|>)) prometheus-alertmanager-0.15.3+ds/ui/app/src/Utils/Date.elm000066400000000000000000000047101341674552200235100ustar00rootroot00000000000000module Utils.Date exposing (..) import ISO8601 import Parser exposing (Parser, (|.), (|=)) import Time import Utils.Types as Types import Tuple import Date import Date.Extra.Format import Date.Extra.Config.Config_en_us exposing (config) parseDuration : String -> Result String Time.Time parseDuration = Parser.run durationParser >> Result.mapError (always "Wrong duration format") durationParser : Parser Time.Time durationParser = Parser.succeed (List.foldr (+) 0) |= Parser.repeat Parser.zeroOrMore term |. Parser.end units : List ( String, number ) units = [ ( "y", 31556926000 ) , ( "d", 86400000 ) , ( "h", 3600000 ) , ( "m", 60000 ) , ( "s", 1000 ) ] timeToString : Time.Time -> String timeToString = round >> ISO8601.fromTime >> ISO8601.toString term : Parser Time.Time term = Parser.map2 (*) Parser.float (units |> List.map (\( unit, ms ) -> Parser.succeed ms |. Parser.symbol unit) |> Parser.oneOf ) |. Parser.ignore Parser.zeroOrMore ((==) ' ') durationFormat : Time.Time -> Maybe String durationFormat time = if time >= 0 then List.foldl (\( unit, ms ) ( result, curr ) -> ( if curr // ms == 0 then result else result ++ toString (curr // ms) ++ unit ++ " " , curr % ms ) ) ( "", round time ) units |> Tuple.first |> String.trim |> Just else Nothing dateFormat : Time.Time -> String dateFormat = Date.fromTime >> (Date.Extra.Format.formatUtc config Date.Extra.Format.isoDateFormat) timeFormat : Time.Time -> String timeFormat = Date.fromTime >> (Date.Extra.Format.formatUtc config Date.Extra.Format.isoTimeFormat) dateTimeFormat : Time.Time -> String dateTimeFormat t = (dateFormat t) ++ " " ++ (timeFormat t) encode : Time.Time -> String encode = round >> ISO8601.fromTime >> ISO8601.toString timeFromString : String -> Result String Time.Time timeFromString string = if string == "" then Err "Should not be empty" else ISO8601.fromString string |> Result.map (ISO8601.toTime >> toFloat) |> Result.mapError (always "Wrong ISO8601 format") fromTime : Time.Time -> Types.Time fromTime time = { s = round time |> ISO8601.fromTime |> ISO8601.toString , t = Just time } prometheus-alertmanager-0.15.3+ds/ui/app/src/Utils/Filter.elm000066400000000000000000000117761341674552200240720ustar00rootroot00000000000000module Utils.Filter exposing ( Matcher , MatchOperator(..) , Filter , nullFilter , generateQueryParam , generateQueryString , stringifyMatcher , stringifyFilter , stringifyGroup , parseGroup , parseFilter , parseMatcher ) import Http exposing (encodeUri) import Parser exposing (Parser, (|.), (|=), zeroOrMore, ignore) import Parser.LanguageKit as Parser exposing (Trailing(..)) import Char import Set type alias Filter = { text : Maybe String , group : Maybe String , receiver : Maybe String , showSilenced : Maybe Bool , showInhibited : Maybe Bool } nullFilter : Filter nullFilter = { text = Nothing , group = Nothing , receiver = Nothing , showSilenced = Nothing , showInhibited = Nothing } generateQueryParam : String -> Maybe String -> Maybe String generateQueryParam name = Maybe.map (encodeUri >> (++) (name ++ "=")) generateQueryString : Filter -> String generateQueryString { receiver, showSilenced, showInhibited, text, group } = let parts = [ ( "silenced", Maybe.withDefault False showSilenced |> toString |> String.toLower |> Just ) , ( "inhibited", Maybe.withDefault False showInhibited |> toString |> String.toLower |> Just ) , ( "filter", emptyToNothing text ) , ( "receiver", emptyToNothing receiver ) , ( "group", group ) ] |> List.filterMap (uncurry generateQueryParam) in if List.length parts > 0 then parts |> String.join "&" |> (++) "?" else "" emptyToNothing : Maybe String -> Maybe String emptyToNothing str = case str of Just "" -> Nothing _ -> str type alias Matcher = { key : String , op : MatchOperator , value : String } type MatchOperator = Eq | NotEq | RegexMatch | NotRegexMatch matchers : List ( String, MatchOperator ) matchers = [ ( "=~", RegexMatch ) , ( "!~", NotRegexMatch ) , ( "=", Eq ) , ( "!=", NotEq ) ] parseFilter : String -> Maybe (List Matcher) parseFilter = Parser.run filter >> Result.toMaybe parseMatcher : String -> Maybe Matcher parseMatcher = Parser.run matcher >> Result.toMaybe stringifyGroup : List String -> Maybe String stringifyGroup list = if List.isEmpty list then Just "" else if list == [ "alertname" ] then Nothing else Just (String.join "," list) parseGroup : Maybe String -> List String parseGroup maybeGroup = case maybeGroup of Nothing -> [ "alertname" ] Just something -> String.split "," something |> List.filter (String.length >> (<) 0) stringifyFilter : List Matcher -> String stringifyFilter matchers = case matchers of [] -> "" list -> (list |> List.map stringifyMatcher |> String.join ", " |> (++) "{" ) ++ "}" stringifyMatcher : Matcher -> String stringifyMatcher { key, op, value } = key ++ (matchers |> List.filter (Tuple.second >> (==) op) |> List.head |> Maybe.map Tuple.first |> Maybe.withDefault "" ) ++ "\"" ++ value ++ "\"" filter : Parser (List Matcher) filter = Parser.succeed identity |= Parser.record spaces item |. Parser.end matcher : Parser Matcher matcher = Parser.succeed identity |. spaces |= item |. spaces |. Parser.end item : Parser Matcher item = Parser.succeed Matcher |= Parser.variable isVarChar isVarChar Set.empty |= (matchers |> List.map (\( keyword, matcher ) -> Parser.succeed matcher |. Parser.keyword keyword ) |> Parser.oneOf ) |= string '"' spaces : Parser () spaces = ignore zeroOrMore (\char -> char == ' ' || char == '\t') string : Char -> Parser String string separator = Parser.succeed identity |. Parser.symbol (String.fromChar separator) |= stringContents separator |. Parser.symbol (String.fromChar separator) stringContents : Char -> Parser String stringContents separator = Parser.oneOf [ Parser.succeed (++) |= keepOne (\char -> char == '\\') |= keepOne (\char -> True) , Parser.keep Parser.oneOrMore (\char -> char /= separator && char /= '\\') ] |> Parser.repeat Parser.zeroOrMore |> Parser.map (String.join "") isVarChar : Char -> Bool isVarChar char = Char.isLower char || Char.isUpper char || (char == '_') || Char.isDigit char keepOne : (Char -> Bool) -> Parser String keepOne = Parser.keep (Parser.Exactly 1) prometheus-alertmanager-0.15.3+ds/ui/app/src/Utils/FormValidation.elm000066400000000000000000000022211341674552200255440ustar00rootroot00000000000000module Utils.FormValidation exposing ( initialField , ValidationState(..) , ValidatedField , validate , fromResult , stringNotEmpty , updateValue ) type ValidationState = Initial | Valid | Invalid String fromResult : Result String a -> ValidationState fromResult result = case result of Ok _ -> Valid Err str -> Invalid str type alias ValidatedField = { value : String , validationState : ValidationState } initialField : String -> ValidatedField initialField value = { value = value , validationState = Initial } updateValue : String -> ValidatedField -> ValidatedField updateValue value field = { field | value = value, validationState = Initial } validate : (String -> Result String a) -> ValidatedField -> ValidatedField validate validator field = { field | validationState = fromResult (validator field.value) } stringNotEmpty : String -> Result String String stringNotEmpty string = if String.isEmpty (String.trim string) then Err "Should not be empty" else Ok string prometheus-alertmanager-0.15.3+ds/ui/app/src/Utils/Keyboard.elm000066400000000000000000000010261341674552200243700ustar00rootroot00000000000000module Utils.Keyboard exposing (keys, onKeyDown, onKeyUp) import Html exposing (Attribute) import Html.Events exposing (on, keyCode) import Json.Decode as Json keys : { backspace : Int , enter : Int , up : Int , down : Int } keys = { backspace = 8 , enter = 13 , up = 38 , down = 40 } onKeyDown : (Int -> msg) -> Attribute msg onKeyDown tagger = on "keydown" (Json.map tagger keyCode) onKeyUp : (Int -> msg) -> Attribute msg onKeyUp tagger = on "keyup" (Json.map tagger keyCode) prometheus-alertmanager-0.15.3+ds/ui/app/src/Utils/List.elm000066400000000000000000000036201341674552200235450ustar00rootroot00000000000000module Utils.List exposing (..) import Utils.Types exposing (Matchers, Matcher) import Dict exposing (Dict) nextElem : a -> List a -> Maybe a nextElem el list = case list of curr :: rest -> if curr == el then List.head rest else nextElem el rest [] -> Nothing lastElem : List a -> Maybe a lastElem = List.foldl (Just >> always) Nothing replaceIf : (a -> Bool) -> a -> List a -> List a replaceIf predicate replacement list = List.map (\item -> if predicate item then replacement else item ) list replaceIndex : Int -> (a -> a) -> List a -> List a replaceIndex index replacement list = List.indexedMap (\currentIndex item -> if index == currentIndex then replacement item else item ) list mjoin : Matchers -> String mjoin m = String.join "," (List.map mstring m) mstring : Matcher -> String mstring m = let sep = if m.isRegex then "=~" else "=" in String.join sep [ m.name, toString m.value ] {-| Takes a key-fn and a list. Creates a `Dict` which maps the key to a list of matching elements. mary = {id=1, name="Mary"} jack = {id=2, name="Jack"} jill = {id=1, name="Jill"} groupBy .id [mary, jack, jill] == Dict.fromList [(1, [mary, jill]), (2, [jack])] Copied from -} groupBy : (a -> comparable) -> List a -> Dict comparable (List a) groupBy keyfn list = List.foldr (\x acc -> Dict.update (keyfn x) (Maybe.map ((::) x) >> Maybe.withDefault [ x ] >> Just) acc ) Dict.empty list zip : List a -> List b -> List ( a, b ) zip a b = List.map2 (,) a b prometheus-alertmanager-0.15.3+ds/ui/app/src/Utils/Match.elm000066400000000000000000000067161341674552200236770ustar00rootroot00000000000000module Utils.Match exposing (jaro, jaroWinkler, consecutiveChars) import Utils.List exposing (zip) import Char {-| Adapted from https://blog.art-of-coding.eu/comparing-strings-with-metrics-in-haskell/ -} jaro : String -> String -> Float jaro s1 s2 = if s1 == s2 then 1.0 else let l1 = String.length s1 l2 = String.length s2 z2 = zip (List.range 1 l2) (String.toList s2) |> List.map (Tuple.mapSecond Char.toCode) searchLength = -- A character must be within searchLength spaces of the -- character we are matching against in order to be considered -- a match. -- (//) is integer division, which removes the need to floor -- the result. ((max l1 l2) // 2) - 1 m = zip (List.range 1 l1) (String.toList s1) |> List.map (Tuple.mapSecond Char.toCode) |> List.concatMap (charMatch searchLength z2) ml = List.length m t = m |> List.map (transposition z2 >> toFloat >> ((*) 0.5)) |> List.sum ml1 = toFloat ml / toFloat l1 ml2 = toFloat ml / toFloat l2 mtm = (toFloat ml - t) / toFloat ml in if ml == 0 then 0 else (1 / 3) * (ml1 + ml2 + mtm) winkler : String -> String -> Float -> Float winkler s1 s2 jaro = if s1 == "" || s2 == "" then 0.0 else if s1 == s2 then 1.0 else let l = consecutiveChars s1 s2 |> String.length |> toFloat p = 0.25 in jaro + ((l * p) * (1.0 - jaro)) jaroWinkler : String -> String -> Float jaroWinkler s1 s2 = if s1 == "" || s2 == "" then 0.0 else if s1 == s2 then 1.0 else jaro s1 s2 |> winkler s1 s2 consecutiveChars : String -> String -> String consecutiveChars s1 s2 = if s1 == "" || s2 == "" then "" else if s1 == s2 then s1 else cp (String.toList s1) (String.toList s2) [] |> String.fromList cp : List Char -> List Char -> List Char -> List Char cp l1 l2 acc = case ( l1, l2 ) of ( x :: xs, y :: ys ) -> if x == y then cp xs ys (acc ++ [ x ]) else if List.length acc > 0 then -- If we have already found matches, we bail. We only want -- consecutive matches. acc else -- Go through every character in l1 until it matches the first -- character in l2, and then start counting from there. cp l1 ys acc _ -> acc charMatch : number -> List ( number, number ) -> ( number, number ) -> List ( number, number ) charMatch matchRange list ( p, q ) = list |> List.drop (p - matchRange - 1) |> List.take (p + matchRange) |> List.filter (Tuple.second >> (==) q) transposition : List ( number, number ) -> ( number, number ) -> Int transposition list ( p, q ) = list |> List.filter (\( x, y ) -> p /= x && q == y ) |> List.length prometheus-alertmanager-0.15.3+ds/ui/app/src/Utils/String.elm000066400000000000000000000034441341674552200241040ustar00rootroot00000000000000module Utils.String exposing (capitalizeFirst, linkify) import String import Char capitalizeFirst : String -> String capitalizeFirst string = case String.uncons string of Nothing -> string Just ( char, rest ) -> String.cons (Char.toUpper char) rest linkify : String -> List (Result String String) linkify string = List.reverse (linkifyHelp (String.words string) []) linkifyHelp : List String -> List (Result String String) -> List (Result String String) linkifyHelp words linkified = case words of [] -> linkified word :: restWords -> if isUrl word then case linkified of (Err lastWord) :: restLinkified -> -- append space to last word linkifyHelp restWords (Ok word :: Err (lastWord ++ " ") :: restLinkified) (Ok lastWord) :: restLinkified -> -- insert space between two links linkifyHelp restWords (Ok word :: Err " " :: linkified) _ -> linkifyHelp restWords (Ok word :: linkified) else case linkified of (Err lastWord) :: restLinkified -> -- concatenate with last word linkifyHelp restWords (Err (lastWord ++ " " ++ word) :: restLinkified) (Ok lastWord) :: restLinkified -> -- insert space after the link linkifyHelp restWords (Err (" " ++ word) :: linkified) _ -> linkifyHelp restWords (Err word :: linkified) isUrl : String -> Bool isUrl = flip String.startsWith >> (flip List.any) [ "http://", "https://" ] prometheus-alertmanager-0.15.3+ds/ui/app/src/Utils/Types.elm000066400000000000000000000007431341674552200237410ustar00rootroot00000000000000module Utils.Types exposing (..) import Time type ApiData a = Initial | Loading | Failure String | Success a type alias Matcher = { isRegex : Bool , name : String , value : String } type alias Matchers = List Matcher type alias Labels = List Label type alias Label = ( String, String ) type alias Time = { t : Maybe Time.Time , s : String } type alias Duration = { d : Maybe Time.Time , s : String } prometheus-alertmanager-0.15.3+ds/ui/app/src/Utils/Views.elm000066400000000000000000000114101341674552200237230ustar00rootroot00000000000000module Utils.Views exposing (..) import Html exposing (..) import Html.Attributes exposing (..) import Html.Events exposing (onCheck, onInput, onClick, onBlur) import Utils.FormValidation exposing (ValidationState(..), ValidatedField) import Utils.String tab : tab -> tab -> (tab -> msg) -> List (Html msg) -> Html msg tab tab currentTab msg content = li [ class "nav-item" ] [ if tab == currentTab then span [ class "nav-link active" ] content else a [ class "nav-link", onClick (msg tab) ] content ] labelButton : Maybe msg -> String -> Html msg labelButton maybeMsg labelText = case maybeMsg of Nothing -> span [ class "btn btn-sm bg-faded btn-secondary mr-2 mb-2" , style [ ( "user-select", "text" ) , ( "-moz-user-select", "text" ) , ( "-webkit-user-select", "text" ) ] ] [ text labelText ] Just msg -> button [ class "btn btn-sm bg-faded btn-secondary mr-2 mb-2" , onClick msg ] [ span [ class "text-muted" ] [ text labelText ] ] linkifyText : String -> List (Html msg) linkifyText str = List.map (\result -> case result of Ok link -> a [ href link, target "_blank" ] [ text link ] Err txt -> text txt ) (Utils.String.linkify str) iconButtonMsg : String -> String -> msg -> Html msg iconButtonMsg classString icon msg = a [ class classString, onClick msg ] [ i [ class <| "fa fa-3 " ++ icon ] [] ] checkbox : String -> Bool -> (Bool -> msg) -> Html msg checkbox name status msg = label [ class "f6 dib mb2 mr2 d-flex align-items-center" ] [ input [ type_ "checkbox", checked status, onCheck msg ] [] , span [ class "pl-2" ] [ text <| " " ++ name ] ] validatedField : (List (Attribute msg) -> List (Html msg) -> Html msg) -> String -> String -> (String -> msg) -> msg -> ValidatedField -> Html msg validatedField htmlField labelText classes inputMsg blurMsg field = case field.validationState of Valid -> div [ class <| "d-flex flex-column form-group has-success " ++ classes ] [ label [] [ strong [] [ text labelText ] ] , htmlField [ value field.value , onInput inputMsg , onBlur blurMsg , class "form-control form-control-success" ] [] ] Initial -> div [ class <| "d-flex flex-column form-group " ++ classes ] [ label [] [ strong [] [ text labelText ] ] , htmlField [ value field.value , onInput inputMsg , onBlur blurMsg , class "form-control" ] [] ] Invalid error -> div [ class <| "d-flex flex-column form-group has-danger " ++ classes ] [ label [] [ strong [] [ text labelText ] ] , htmlField [ value field.value , onInput inputMsg , onBlur blurMsg , class "form-control form-control-danger" ] [] , div [ class "form-control-feedback" ] [ text error ] ] formField : String -> String -> String -> (String -> msg) -> Html msg formField labelText content classes msg = div [ class <| "d-flex flex-column " ++ classes ] [ label [] [ strong [] [ text labelText ] ] , input [ value content, onInput msg ] [] ] textField : String -> String -> String -> (String -> msg) -> Html msg textField labelText content classes msg = div [ class <| "d-flex flex-column " ++ classes ] [ label [] [ strong [] [ text labelText ] ] , textarea [ value content, onInput msg ] [] ] buttonLink : String -> String -> String -> msg -> Html msg buttonLink icon link color msg = a [ class <| "" ++ color, href link, onClick msg ] [ i [ class <| "" ++ icon ] [] ] formInput : String -> String -> (String -> msg) -> Html msg formInput inputValue classes msg = Html.input [ class <| "w-100 " ++ classes, value inputValue, onInput msg ] [] loading : Html msg loading = div [] [ i [ class "fa fa-cog fa-spin fa-3x fa-fw" ] [] , span [ class "sr-only" ] [ text "Loading..." ] ] error : String -> Html msg error err = div [ class "alert alert-warning" ] [ text (Utils.String.capitalizeFirst err) ] prometheus-alertmanager-0.15.3+ds/ui/app/src/Views.elm000066400000000000000000000055461341674552200226400ustar00rootroot00000000000000module Views exposing (..) import Html exposing (Html, node, text, div) import Html.Attributes exposing (class, rel, href, src, style) import Html.Events exposing (on) import Json.Decode exposing (succeed) import Types exposing (Msg(MsgForSilenceForm, MsgForSilenceView, BootstrapCSSLoaded, FontAwesomeCSSLoaded), Model, Route(..)) import Utils.Views exposing (error, loading) import Utils.Types exposing (ApiData(Failure, Success)) import Views.SilenceList.Views as SilenceList import Views.SilenceForm.Views as SilenceForm import Views.AlertList.Views as AlertList import Views.SilenceView.Views as SilenceView import Views.NotFound.Views as NotFound import Views.Status.Views as Status import Views.NavBar.Views exposing (navBar) view : Model -> Html Msg view model = div [] [ renderCSS model.libUrl , case ( model.bootstrapCSS, model.fontAwesomeCSS ) of ( Success _, Success _ ) -> div [] [ navBar model.route , div [ class "container pb-4" ] [ currentView model ] ] ( Failure err, _ ) -> failureView model err ( _, Failure err ) -> failureView model err _ -> text "" ] failureView : Model -> String -> Html Msg failureView model err = div [] [ div [ style [ ( "padding", "40px" ), ( "color", "red" ) ] ] [ text err ] , navBar model.route , div [ class "container pb-4" ] [ currentView model ] ] renderCSS : String -> Html Msg renderCSS assetsUrl = div [] [ cssNode (assetsUrl ++ "lib/bootstrap-4.0.0-alpha.6-dist/css/bootstrap.min.css") BootstrapCSSLoaded , cssNode (assetsUrl ++ "lib/font-awesome-4.7.0/css/font-awesome.min.css") FontAwesomeCSSLoaded ] cssNode : String -> (ApiData String -> Msg) -> Html Msg cssNode url msg = node "link" [ href url , rel "stylesheet" , on "load" (succeed (msg (Success url))) , on "error" (succeed (msg (Failure ("Failed to load CSS from: " ++ url)))) ] [] currentView : Model -> Html Msg currentView model = case model.route of StatusRoute -> Status.view model.status SilenceViewRoute silenceId -> SilenceView.view model.silenceView AlertsRoute filter -> AlertList.view model.alertList filter SilenceListRoute _ -> SilenceList.view model.silenceList SilenceFormNewRoute matchers -> SilenceForm.view Nothing matchers model.defaultCreator model.silenceForm |> Html.map MsgForSilenceForm SilenceFormEditRoute silenceId -> SilenceForm.view (Just silenceId) [] "" model.silenceForm |> Html.map MsgForSilenceForm TopLevelRoute -> Utils.Views.loading NotFoundRoute -> NotFound.view prometheus-alertmanager-0.15.3+ds/ui/app/src/Views/000077500000000000000000000000001341674552200221275ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/ui/app/src/Views/AlertList/000077500000000000000000000000001341674552200240325ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/ui/app/src/Views/AlertList/AlertView.elm000066400000000000000000000120101341674552200264250ustar00rootroot00000000000000module Views.AlertList.AlertView exposing (view, addLabelMsg) import Alerts.Types exposing (Alert) import Html exposing (..) import Html.Attributes exposing (class, style, href, value, readonly, title) import Html.Events exposing (onClick) import Types exposing (Msg(Noop, MsgForAlertList)) import Utils.Date import Views.FilterBar.Types as FilterBarTypes import Views.AlertList.Types exposing (AlertListMsg(MsgForFilterBar, SetActive)) import Utils.Views import Utils.Filter import Views.SilenceForm.Parsing exposing (newSilenceFromAlertLabels) view : List ( String, String ) -> Maybe String -> Alert -> Html Msg view labels maybeActiveId alert = let -- remove the grouping labels, and bring the alertname to front ungroupedLabels = alert.labels |> List.filter ((flip List.member) labels >> not) |> List.partition (Tuple.first >> (==) "alertname") |> uncurry (++) in li [ -- speedup rendering in Chrome, because list-group-item className -- creates a new layer in the rendering engine style [ ( "position", "static" ) ] , class "align-items-start list-group-item border-0 p-0 mb-4" ] [ div [ class "w-100 mb-2 d-flex align-items-start" ] [ titleView alert , if List.length alert.annotations > 0 then annotationsButton maybeActiveId alert else text "" , generatorUrlButton alert.generatorUrl , silenceButton alert ] , if maybeActiveId == Just alert.id then table [ class "table w-100 mb-1" ] (List.map annotation alert.annotations) else text "" , div [] (List.map labelButton ungroupedLabels) ] titleView : Alert -> Html Msg titleView { startsAt, isInhibited } = let ( className, inhibited ) = if isInhibited then ( "text-muted", " (inhibited)" ) else ( "", "" ) in span [ class ("align-self-center mr-2 " ++ className) ] [ text (Utils.Date.timeFormat startsAt ++ ", " ++ Utils.Date.dateFormat startsAt ++ inhibited ) ] annotationsButton : Maybe String -> Alert -> Html Msg annotationsButton maybeActiveId alert = if maybeActiveId == Just alert.id then button [ onClick (SetActive Nothing |> MsgForAlertList) , class "btn btn-outline-info border-0 active" ] [ i [ class "fa fa-minus mr-2" ] [], text "Info" ] else button [ onClick (SetActive (Just alert.id) |> MsgForAlertList) , class "btn btn-outline-info border-0" ] [ i [ class "fa fa-plus mr-2" ] [], text "Info" ] annotation : ( String, String ) -> Html Msg annotation ( key, value ) = tr [] [ th [ class "text-nowrap" ] [ text (key ++ ":") ] , td [ class "w-100" ] (Utils.Views.linkifyText value) ] labelButton : ( String, String ) -> Html Msg labelButton ( key, val ) = div [ class "btn-group mr-2 mb-2" ] [ span [ class "btn btn-sm border-right-0 text-muted" -- have to reset bootstrap button styles to make the text selectable , style [ ( "user-select", "initial" ) , ( "-moz-user-select", "initial" ) , ( "-webkit-user-select", "initial" ) , ( "border-color", "#ccc" ) ] ] [ text (key ++ "=\"" ++ val ++ "\"") ] , button [ class "btn btn-sm bg-faded btn-outline-secondary" , onClick (addLabelMsg ( key, val )) , title "Filter by this label" ] [ text "+" ] ] addLabelMsg : ( String, String ) -> Msg addLabelMsg ( key, value ) = FilterBarTypes.AddFilterMatcher False { key = key , op = Utils.Filter.Eq , value = value } |> MsgForFilterBar |> MsgForAlertList silenceButton : Alert -> Html Msg silenceButton alert = case alert.silenceId of Just sId -> a [ class "btn btn-outline-danger border-0" , href ("#/silences/" ++ sId) ] [ i [ class "fa fa-bell-slash mr-2" ] [] , text "Silenced" ] Nothing -> a [ class "btn btn-outline-info border-0" , href (newSilenceFromAlertLabels alert.labels) ] [ i [ class "fa fa-bell-slash-o mr-2" ] [] , text "Silence" ] generatorUrlButton : String -> Html Msg generatorUrlButton url = a [ class "btn btn-outline-info border-0", href url ] [ i [ class "fa fa-line-chart mr-2" ] [] , text "Source" ] prometheus-alertmanager-0.15.3+ds/ui/app/src/Views/AlertList/Filter.elm000066400000000000000000000043301341674552200257560ustar00rootroot00000000000000module Views.AlertList.Filter exposing (matchers) import Alerts.Types exposing (Alert, AlertGroup, Block) import Utils.Types exposing (Matchers) import Regex exposing (regex, contains) matchers : Maybe Utils.Types.Matchers -> List AlertGroup -> List AlertGroup matchers matchers alerts = case matchers of Just ms -> by (filterAlertGroupLabels ms) alerts Nothing -> alerts alertsFromBlock : (Alert -> Bool) -> Block -> Maybe Block alertsFromBlock fn block = let alerts = List.filter fn block.alerts in if not <| List.isEmpty alerts then Just { block | alerts = alerts } else Nothing byLabel : Utils.Types.Matchers -> Block -> Maybe Block byLabel matchers block = alertsFromBlock (\a -> -- Check that all labels are present within the alert's label set. List.all (\m -> -- Check for regex or direct match if m.isRegex then -- Check if key is present, then regex match value. let x = List.head <| List.filter (\( key, value ) -> key == m.name) a.labels regex = Regex.regex m.value in -- No regex match case x of Just ( _, value ) -> Regex.contains regex value Nothing -> False else List.member ( m.name, m.value ) a.labels ) matchers ) block filterAlertGroupLabels : Utils.Types.Matchers -> AlertGroup -> Maybe AlertGroup filterAlertGroupLabels matchers alertGroup = let blocks = List.filterMap (byLabel matchers) alertGroup.blocks in if not <| List.isEmpty blocks then Just { alertGroup | blocks = blocks } else Nothing by : (a -> Maybe a) -> List a -> List a by fn groups = List.filterMap fn groups prometheus-alertmanager-0.15.3+ds/ui/app/src/Views/AlertList/Parsing.elm000066400000000000000000000012261341674552200261350ustar00rootroot00000000000000module Views.AlertList.Parsing exposing (alertsParser) import UrlParser exposing ((), (), Parser, int, map, oneOf, parseHash, s, string, stringParam) import Utils.Filter exposing (Filter, parseMatcher, MatchOperator(RegexMatch)) boolParam : String -> UrlParser.QueryParser (Maybe Bool -> a) a boolParam name = UrlParser.customParam name (Maybe.map (String.toLower >> (/=) "false")) alertsParser : Parser (Filter -> a) a alertsParser = s "alerts" stringParam "filter" stringParam "group" stringParam "receiver" boolParam "silenced" boolParam "inhibited" |> map Filter prometheus-alertmanager-0.15.3+ds/ui/app/src/Views/AlertList/Types.elm000066400000000000000000000021051341674552200256330ustar00rootroot00000000000000module Views.AlertList.Types exposing (AlertListMsg(..), Model, Tab(..), initAlertList) import Alerts.Types exposing (Alert) import Utils.Types exposing (ApiData(Initial)) import Views.FilterBar.Types as FilterBar import Views.GroupBar.Types as GroupBar import Views.ReceiverBar.Types as ReceiverBar type AlertListMsg = AlertsFetched (ApiData (List Alert)) | FetchAlerts | MsgForReceiverBar ReceiverBar.Msg | MsgForFilterBar FilterBar.Msg | MsgForGroupBar GroupBar.Msg | ToggleSilenced Bool | ToggleInhibited Bool | SetActive (Maybe String) | SetTab Tab type Tab = FilterTab | GroupTab type alias Model = { alerts : ApiData (List Alert) , receiverBar : ReceiverBar.Model , groupBar : GroupBar.Model , filterBar : FilterBar.Model , tab : Tab , activeId : Maybe String } initAlertList : Model initAlertList = { alerts = Initial , receiverBar = ReceiverBar.initReceiverBar , groupBar = GroupBar.initGroupBar , filterBar = FilterBar.initFilterBar , tab = FilterTab , activeId = Nothing } prometheus-alertmanager-0.15.3+ds/ui/app/src/Views/AlertList/Updates.elm000066400000000000000000000071621341674552200261440ustar00rootroot00000000000000module Views.AlertList.Updates exposing (..) import Alerts.Api as Api import Views.AlertList.Types exposing (AlertListMsg(..), Model, Tab(FilterTab, GroupTab)) import Views.FilterBar.Updates as FilterBar import Utils.Filter exposing (Filter, parseFilter) import Utils.Types exposing (ApiData(Initial, Loading, Success, Failure)) import Types exposing (Msg(MsgForAlertList, Noop)) import Set import Navigation import Utils.Filter exposing (generateQueryString) import Views.GroupBar.Updates as GroupBar import Views.ReceiverBar.Updates as ReceiverBar update : AlertListMsg -> Model -> Filter -> String -> String -> ( Model, Cmd Types.Msg ) update msg ({ groupBar, filterBar, receiverBar } as model) filter apiUrl basePath = let alertsUrl = basePath ++ "#/alerts" in case msg of AlertsFetched listOfAlerts -> ( { model | alerts = listOfAlerts , groupBar = case listOfAlerts of Success alerts -> { groupBar | list = List.concatMap .labels alerts |> List.map Tuple.first |> Set.fromList } _ -> groupBar } , Cmd.none ) FetchAlerts -> let newGroupBar = GroupBar.setFields filter groupBar newFilterBar = FilterBar.setMatchers filter filterBar in ( { model | alerts = Loading, filterBar = newFilterBar, groupBar = newGroupBar, activeId = Nothing } , Cmd.batch [ Api.fetchAlerts apiUrl filter |> Cmd.map (AlertsFetched >> MsgForAlertList) , ReceiverBar.fetchReceivers apiUrl |> Cmd.map (MsgForReceiverBar >> MsgForAlertList) ] ) ToggleSilenced showSilenced -> ( model , Navigation.newUrl (alertsUrl ++ generateQueryString { filter | showSilenced = Just showSilenced }) ) ToggleInhibited showInhibited -> ( model , Navigation.newUrl (alertsUrl ++ generateQueryString { filter | showInhibited = Just showInhibited }) ) SetTab tab -> ( { model | tab = tab }, Cmd.none ) MsgForFilterBar msg -> let ( newFilterBar, cmd ) = FilterBar.update alertsUrl filter msg filterBar in ( { model | filterBar = newFilterBar, tab = FilterTab }, Cmd.map (MsgForFilterBar >> MsgForAlertList) cmd ) MsgForGroupBar msg -> let ( newGroupBar, cmd ) = GroupBar.update alertsUrl filter msg groupBar in ( { model | groupBar = newGroupBar }, Cmd.map (MsgForGroupBar >> MsgForAlertList) cmd ) MsgForReceiverBar msg -> let ( newReceiverBar, cmd ) = ReceiverBar.update alertsUrl filter msg receiverBar in ( { model | receiverBar = newReceiverBar }, Cmd.map (MsgForReceiverBar >> MsgForAlertList) cmd ) SetActive maybeId -> ( { model | activeId = maybeId }, Cmd.none ) prometheus-alertmanager-0.15.3+ds/ui/app/src/Views/AlertList/Views.elm000066400000000000000000000124061341674552200256310ustar00rootroot00000000000000module Views.AlertList.Views exposing (view) import Alerts.Types exposing (Alert) import Html exposing (..) import Html.Attributes exposing (..) import Html.Events exposing (..) import Types exposing (Msg(Noop, MsgForAlertList)) import Utils.Filter exposing (Filter) import Views.FilterBar.Views as FilterBar import Views.ReceiverBar.Views as ReceiverBar import Utils.Types exposing (ApiData(Initial, Success, Loading, Failure), Labels) import Utils.Views import Utils.List import Views.AlertList.AlertView as AlertView import Views.GroupBar.Types as GroupBar import Views.AlertList.Types exposing (AlertListMsg(..), Model, Tab(..)) import Types exposing (Msg(Noop, MsgForAlertList)) import Views.GroupBar.Views as GroupBar import Dict exposing (Dict) renderCheckbox : String -> Maybe Bool -> (Bool -> AlertListMsg) -> Html Msg renderCheckbox textLabel maybeShowSilenced toggleMsg = li [ class "nav-item" ] [ label [ class "mt-1 ml-1 custom-control custom-checkbox" ] [ input [ type_ "checkbox" , class "custom-control-input" , checked (Maybe.withDefault False maybeShowSilenced) , onCheck (toggleMsg >> MsgForAlertList) ] [] , span [ class "custom-control-indicator" ] [] , span [ class "custom-control-description" ] [ text textLabel ] ] ] view : Model -> Filter -> Html Msg view { alerts, groupBar, filterBar, receiverBar, tab, activeId } filter = div [] [ div [ class "card mb-5" ] [ div [ class "card-header" ] [ ul [ class "nav nav-tabs card-header-tabs" ] [ Utils.Views.tab FilterTab tab (SetTab >> MsgForAlertList) [ text "Filter" ] , Utils.Views.tab GroupTab tab (SetTab >> MsgForAlertList) [ text "Group" ] , receiverBar |> ReceiverBar.view filter.receiver |> Html.map (MsgForReceiverBar >> MsgForAlertList) , renderCheckbox "Silenced" filter.showSilenced ToggleSilenced , renderCheckbox "Inhibited" filter.showInhibited ToggleInhibited ] ] , div [ class "card-block" ] [ case tab of FilterTab -> Html.map (MsgForFilterBar >> MsgForAlertList) (FilterBar.view filterBar) GroupTab -> Html.map (MsgForGroupBar >> MsgForAlertList) (GroupBar.view groupBar) ] ] , case alerts of Success alerts -> alertGroups activeId filter groupBar alerts Loading -> Utils.Views.loading Initial -> Utils.Views.loading Failure msg -> Utils.Views.error msg ] alertGroups : Maybe String -> Filter -> GroupBar.Model -> List Alert -> Html Msg alertGroups activeId filter { fields } alerts = let grouped = alerts |> Utils.List.groupBy (.labels >> List.filter (\( key, _ ) -> List.member key fields)) in grouped |> Dict.keys |> List.partition ((/=) []) |> uncurry (++) |> List.filterMap (\labels -> Maybe.map (alertList activeId labels filter) (Dict.get labels grouped) ) |> (\list -> if List.isEmpty list then [ Utils.Views.error "No alerts found" ] else list ) |> div [] alertList : Maybe String -> Labels -> Filter -> List Alert -> Html Msg alertList activeId labels filter alerts = div [] [ div [] (case labels of [] -> [ span [ class "btn btn-secondary mr-1 mb-3" ] [ text "Not grouped" ] ] _ -> List.map (\( key, value ) -> div [ class "btn-group mr-1 mb-3" ] [ span [ class "btn text-muted" , style [ ( "user-select", "initial" ) , ( "-moz-user-select", "initial" ) , ( "-webkit-user-select", "initial" ) , ( "border-color", "#5bc0de" ) ] ] [ text (key ++ "=\"" ++ value ++ "\"") ] , button [ class "btn btn-outline-info" , onClick (AlertView.addLabelMsg ( key, value )) , title "Filter by this label" ] [ text "+" ] ] ) labels ) , ul [ class "list-group mb-4" ] (List.map (AlertView.view labels activeId) alerts) ] prometheus-alertmanager-0.15.3+ds/ui/app/src/Views/FilterBar/000077500000000000000000000000001341674552200240015ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/ui/app/src/Views/FilterBar/Types.elm000066400000000000000000000020151341674552200256020ustar00rootroot00000000000000module Views.FilterBar.Types exposing (Model, Msg(..), initFilterBar) import Utils.Filter type alias Model = { matchers : List Utils.Filter.Matcher , backspacePressed : Bool , matcherText : String } type Msg = AddFilterMatcher Bool Utils.Filter.Matcher | DeleteFilterMatcher Bool Utils.Filter.Matcher | PressingBackspace Bool | UpdateMatcherText String | Noop {-| A note about the `backspacePressed` attribute: Holding down the backspace removes (one by one) each last character in the input, and the whole time sends multiple keyDown events. This is a guard so that if a user holds down backspace to remove the text in the input, they won't accidentally hold backspace too long and then delete the preceding matcher as well. So, once a user holds backspace to clear an input, they have to then lift up the key and press it again to proceed to deleting the next matcher. -} initFilterBar : Model initFilterBar = { matchers = [] , backspacePressed = False , matcherText = "" } prometheus-alertmanager-0.15.3+ds/ui/app/src/Views/FilterBar/Updates.elm000066400000000000000000000043621341674552200261120ustar00rootroot00000000000000module Views.FilterBar.Updates exposing (update, setMatchers) import Views.FilterBar.Types exposing (Msg(..), Model) import Task import Dom import Navigation import Utils.Filter exposing (Filter, generateQueryString, stringifyFilter, parseFilter) update : String -> Filter -> Msg -> Model -> ( Model, Cmd Msg ) update url filter msg model = case msg of AddFilterMatcher emptyMatcherText matcher -> immediatelyFilter url filter { model | matchers = if List.member matcher model.matchers then model.matchers else model.matchers ++ [ matcher ] , matcherText = if emptyMatcherText then "" else model.matcherText } DeleteFilterMatcher setMatcherText matcher -> immediatelyFilter url filter { model | matchers = List.filter ((/=) matcher) model.matchers , matcherText = if setMatcherText then Utils.Filter.stringifyMatcher matcher else model.matcherText } UpdateMatcherText value -> ( { model | matcherText = value }, Cmd.none ) PressingBackspace isPressed -> ( { model | backspacePressed = isPressed }, Cmd.none ) Noop -> ( model, Cmd.none ) immediatelyFilter : String -> Filter -> Model -> ( Model, Cmd Msg ) immediatelyFilter url filter model = let newFilter = { filter | text = Just (stringifyFilter model.matchers) } in ( { model | matchers = [] } , Cmd.batch [ Navigation.newUrl (url ++ generateQueryString newFilter) , Dom.focus "filter-bar-matcher" |> Task.attempt (always Noop) ] ) setMatchers : Filter -> Model -> Model setMatchers filter model = { model | matchers = filter.text |> Maybe.andThen parseFilter |> Maybe.withDefault [] } prometheus-alertmanager-0.15.3+ds/ui/app/src/Views/FilterBar/Views.elm000066400000000000000000000102751341674552200256020ustar00rootroot00000000000000module Views.FilterBar.Views exposing (view) import Html exposing (Html, Attribute, div, span, input, text, button, i, small) import Html.Attributes exposing (value, class, style, disabled, id) import Html.Events exposing (onClick, onInput, on, keyCode) import Utils.Filter exposing (Matcher) import Utils.List import Utils.Keyboard exposing (keys, onKeyUp, onKeyDown) import Views.FilterBar.Types exposing (Msg(..), Model) keys : { backspace : Int , enter : Int } keys = { backspace = 8 , enter = 13 } viewMatcher : Matcher -> Html Msg viewMatcher matcher = div [ class "col col-auto" ] [ div [ class "btn-group mr-2 mb-2" ] [ button [ class "btn btn-outline-info" , onClick (DeleteFilterMatcher True matcher) ] [ text <| Utils.Filter.stringifyMatcher matcher ] , button [ class "btn btn-outline-danger" , onClick (DeleteFilterMatcher False matcher) ] [ text "×" ] ] ] viewMatchers : List Matcher -> List (Html Msg) viewMatchers matchers = matchers |> List.map viewMatcher view : Model -> Html Msg view { matchers, matcherText, backspacePressed } = let maybeMatcher = Utils.Filter.parseMatcher matcherText maybeLastMatcher = Utils.List.lastElem matchers className = if matcherText == "" then "" else case maybeMatcher of Just _ -> "has-success" Nothing -> "has-danger" keyDown key = if key == keys.enter then maybeMatcher |> Maybe.map (AddFilterMatcher True) |> Maybe.withDefault Noop else if key == keys.backspace then if matcherText == "" then case ( backspacePressed, maybeLastMatcher ) of ( False, Just lastMatcher ) -> DeleteFilterMatcher True lastMatcher _ -> Noop else PressingBackspace True else Noop keyUp key = if key == keys.backspace then PressingBackspace False else Noop onClickAttr = maybeMatcher |> Maybe.map (AddFilterMatcher True) |> Maybe.withDefault Noop |> onClick isDisabled = maybeMatcher == Nothing in div [ class "row no-gutters align-items-start" ] (viewMatchers matchers ++ [ div [ class ("col " ++ className) , style [ ( "min-width", "200px" ) ] ] [ div [ class "input-group" ] [ input [ id "filter-bar-matcher" , class "form-control" , value matcherText , onKeyDown keyDown , onKeyUp keyUp , onInput UpdateMatcherText ] [] , span [ class "input-group-btn" ] [ button [ class "btn btn-primary", disabled isDisabled, onClickAttr ] [ text "+" ] ] ] , small [ class "form-text text-muted" ] [ text "Custom matcher, e.g." , button [ class "btn btn-link btn-sm align-baseline" , onClick (UpdateMatcherText exampleMatcher) ] [ text exampleMatcher ] ] ] ] ) exampleMatcher : String exampleMatcher = "env=\"production\"" prometheus-alertmanager-0.15.3+ds/ui/app/src/Views/GroupBar/000077500000000000000000000000001341674552200236505ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/ui/app/src/Views/GroupBar/Types.elm000066400000000000000000000014331341674552200254540ustar00rootroot00000000000000module Views.GroupBar.Types exposing (Model, Msg(..), initGroupBar) import Set exposing (Set) type alias Model = { list : Set String , fieldText : String , fields : List String , matches : List String , backspacePressed : Bool , focused : Bool , resultsHovered : Bool , maybeSelectedMatch : Maybe String } type Msg = AddField Bool String | DeleteField Bool String | Select (Maybe String) | PressingBackspace Bool | Focus Bool | ResultsHovered Bool | UpdateFieldText String | Noop initGroupBar : Model initGroupBar = { list = Set.empty , fieldText = "" , fields = [] , matches = [] , focused = False , resultsHovered = False , backspacePressed = False , maybeSelectedMatch = Nothing } prometheus-alertmanager-0.15.3+ds/ui/app/src/Views/GroupBar/Updates.elm000066400000000000000000000065171341674552200257650ustar00rootroot00000000000000module Views.GroupBar.Updates exposing (update, setFields) import Views.GroupBar.Types exposing (Model, Msg(..)) import Utils.Match exposing (jaroWinkler) import Task import Dom import Set import Utils.Filter exposing (Filter, generateQueryString, stringifyGroup, parseGroup) import Navigation update : String -> Filter -> Msg -> Model -> ( Model, Cmd Msg ) update url filter msg model = case msg of AddField emptyFieldText text -> immediatelyFilter url filter { model | fields = model.fields ++ [ text ] , matches = [] , fieldText = if emptyFieldText then "" else model.fieldText } DeleteField setFieldText text -> immediatelyFilter url filter { model | fields = List.filter ((/=) text) model.fields , matches = [] , fieldText = if setFieldText then text else model.fieldText } Select maybeSelectedMatch -> ( { model | maybeSelectedMatch = maybeSelectedMatch }, Cmd.none ) Focus focused -> ( { model | focused = focused , maybeSelectedMatch = Nothing } , Cmd.none ) ResultsHovered resultsHovered -> ( { model | resultsHovered = resultsHovered } , Cmd.none ) PressingBackspace pressed -> ( { model | backspacePressed = pressed }, Cmd.none ) UpdateFieldText text -> updateAutoComplete { model | fieldText = text } Noop -> ( model, Cmd.none ) immediatelyFilter : String -> Filter -> Model -> ( Model, Cmd Msg ) immediatelyFilter url filter model = let newFilter = { filter | group = stringifyGroup model.fields } in ( model , Cmd.batch [ Navigation.newUrl (url ++ generateQueryString newFilter) , Dom.focus "group-by-field" |> Task.attempt (always Noop) ] ) setFields : Filter -> Model -> Model setFields filter model = { model | fields = parseGroup filter.group } updateAutoComplete : Model -> ( Model, Cmd Msg ) updateAutoComplete model = ( { model | matches = if String.isEmpty model.fieldText then [] else if String.contains " " model.fieldText then model.matches else -- TODO: How many matches do we want to show? -- NOTE: List.reverse is used because our scale is (0.0, 1.0), -- but we want the higher values to be in the front of the -- list. Set.toList model.list |> List.filter ((flip List.member model.fields) >> not) |> List.sortBy (jaroWinkler model.fieldText) |> List.reverse |> List.take 10 , maybeSelectedMatch = Nothing } , Cmd.none ) prometheus-alertmanager-0.15.3+ds/ui/app/src/Views/GroupBar/Views.elm000066400000000000000000000130571341674552200254520ustar00rootroot00000000000000module Views.GroupBar.Views exposing (view) import Views.GroupBar.Types exposing (Msg(..), Model) import Html exposing (Html, Attribute, div, span, input, text, button, i, small, ul, li, a) import Html.Attributes exposing (value, class, style, disabled, id, href) import Html.Events exposing (onClick, onInput, on, keyCode, onFocus, onBlur, onMouseEnter, onMouseLeave) import Set import Utils.List import Utils.Keyboard exposing (keys, onKeyUp, onKeyDown) view : Model -> Html Msg view ({ list, fieldText, fields } as model) = let isDisabled = not (Set.member fieldText list) || List.member fieldText fields className = if String.isEmpty fieldText then "" else if isDisabled then "has-danger" else "has-success" in div [ class "row no-gutters align-items-start" ] (List.map viewField fields ++ [ div [ class ("col " ++ className) , style [ ( "min-width", "200px" ) ] ] [ textInputField isDisabled model , exampleField fields , autoCompleteResults model ] ] ) exampleField : List String -> Html Msg exampleField fields = if List.member "alertname" fields then small [ class "form-text text-muted" ] [ text "Label key for grouping alerts" ] else small [ class "form-text text-muted" ] [ text "Label key for grouping alerts, e.g." , button [ class "btn btn-link btn-sm align-baseline" , onClick (UpdateFieldText "alertname") ] [ text "alertname" ] ] textInputField : Bool -> Model -> Html Msg textInputField isDisabled { fieldText, matches, maybeSelectedMatch, fields, backspacePressed } = let onClickMsg = if isDisabled then Noop else AddField True fieldText nextMatch = maybeSelectedMatch |> Maybe.map (flip Utils.List.nextElem <| matches) |> Maybe.withDefault (List.head matches) prevMatch = maybeSelectedMatch |> Maybe.map (flip Utils.List.nextElem <| List.reverse matches) |> Maybe.withDefault (Utils.List.lastElem matches) keyDown key = if key == keys.down then Select nextMatch else if key == keys.up then Select prevMatch else if key == keys.enter then if not isDisabled then AddField True fieldText else maybeSelectedMatch |> Maybe.map (AddField True) |> Maybe.withDefault Noop else if key == keys.backspace then if fieldText == "" then case ( Utils.List.lastElem fields, backspacePressed ) of ( Just lastField, False ) -> DeleteField True lastField _ -> Noop else PressingBackspace True else Noop keyUp key = if key == keys.backspace then PressingBackspace False else Noop in div [ class "input-group" ] [ input [ id "group-by-field" , class "form-control" , value fieldText , onKeyDown keyDown , onKeyUp keyUp , onInput UpdateFieldText , onFocus (Focus True) , onBlur (Focus False) ] [] , span [ class "input-group-btn" ] [ button [ class "btn btn-primary", disabled isDisabled, onClick onClickMsg ] [ text "+" ] ] ] autoCompleteResults : Model -> Html Msg autoCompleteResults { maybeSelectedMatch, focused, resultsHovered, matches } = let autoCompleteClass = if (focused || resultsHovered) && not (List.isEmpty matches) then "show" else "" in div [ class ("autocomplete-menu " ++ autoCompleteClass) , onMouseEnter (ResultsHovered True) , onMouseLeave (ResultsHovered False) ] [ matches |> List.map (matchedField maybeSelectedMatch) |> div [ class "dropdown-menu" ] ] matchedField : Maybe String -> String -> Html Msg matchedField maybeSelectedMatch field = let className = if maybeSelectedMatch == Just field then "active" else "" in button [ class ("dropdown-item " ++ className) , onClick (AddField True field) ] [ text field ] viewField : String -> Html Msg viewField field = div [ class "col col-auto" ] [ div [ class "btn-group mr-2 mb-2" ] [ button [ class "btn btn-outline-info" , onClick (DeleteField True field) ] [ text field ] , button [ class "btn btn-outline-danger" , onClick (DeleteField False field) ] [ text "×" ] ] ] prometheus-alertmanager-0.15.3+ds/ui/app/src/Views/NavBar/000077500000000000000000000000001341674552200233005ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/ui/app/src/Views/NavBar/Types.elm000066400000000000000000000007711341674552200251100ustar00rootroot00000000000000module Views.NavBar.Types exposing (Tab, alertsTab, silencesTab, statusTab, noneTab, tabs) type alias Tab = { link : String , name : String } alertsTab : Tab alertsTab = { link = "#/alerts", name = "Alerts" } silencesTab : Tab silencesTab = { link = "#/silences", name = "Silences" } statusTab : Tab statusTab = { link = "#/status", name = "Status" } noneTab : Tab noneTab = { link = "", name = "" } tabs : List Tab tabs = [ alertsTab, silencesTab, statusTab ] prometheus-alertmanager-0.15.3+ds/ui/app/src/Views/NavBar/Views.elm000066400000000000000000000042361341674552200251010ustar00rootroot00000000000000module Views.NavBar.Views exposing (navBar) import Html exposing (Html, header, text, a, nav, ul, li, div) import Html.Attributes exposing (class, href, title, style) import Types exposing (Route(..)) import Views.NavBar.Types exposing (Tab, alertsTab, silencesTab, statusTab, noneTab, tabs) navBar : Route -> Html msg navBar currentRoute = header [ class "navbar navbar-toggleable-md navbar-light bg-faded mb-5 pt-3 pb-3" , style [ ( "border-bottom", "1px solid rgba(0, 0, 0, .125)" ) ] ] [ nav [ class "container" ] [ a [ class "navbar-brand", href "#" ] [ text "Alertmanager" ] , ul [ class "navbar-nav" ] (navBarItems currentRoute) , case currentRoute of SilenceFormEditRoute _ -> text "" SilenceFormNewRoute _ -> text "" _ -> div [ class "form-inline ml-auto" ] [ a [ class "btn btn-outline-info" , href "#/silences/new" ] [ text "New Silence" ] ] ] ] navBarItems : Route -> List (Html msg) navBarItems currentRoute = List.map (navBarItem currentRoute) tabs navBarItem : Route -> Tab -> Html msg navBarItem currentRoute tab = li [ class <| "nav-item" ++ (isActive currentRoute tab) ] [ a [ class "nav-link", href tab.link, title tab.name ] [ text tab.name ] ] isActive : Route -> Tab -> String isActive currentRoute tab = if routeToTab currentRoute == tab then " active" else "" routeToTab : Route -> Tab routeToTab currentRoute = case currentRoute of AlertsRoute _ -> alertsTab NotFoundRoute -> noneTab SilenceFormEditRoute _ -> silencesTab SilenceFormNewRoute _ -> silencesTab SilenceListRoute _ -> silencesTab SilenceViewRoute _ -> silencesTab StatusRoute -> statusTab TopLevelRoute -> noneTab prometheus-alertmanager-0.15.3+ds/ui/app/src/Views/NotFound/000077500000000000000000000000001341674552200236635ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/ui/app/src/Views/NotFound/Views.elm000066400000000000000000000003071341674552200254570ustar00rootroot00000000000000module Views.NotFound.Views exposing (view) import Html exposing (Html, div, h1, text) import Types exposing (Msg) view : Html Msg view = div [] [ h1 [] [ text "not found" ] ] prometheus-alertmanager-0.15.3+ds/ui/app/src/Views/ReceiverBar/000077500000000000000000000000001341674552200243205ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/ui/app/src/Views/ReceiverBar/Types.elm000066400000000000000000000014411341674552200261230ustar00rootroot00000000000000module Views.ReceiverBar.Types exposing (Model, Msg(..), initReceiverBar) import Utils.Types exposing (ApiData(Initial)) import Alerts.Types exposing (Receiver) type Msg = ReceiversFetched (ApiData (List Receiver)) | UpdateReceiver String | EditReceivers | FilterByReceiver String | Select (Maybe Receiver) | ResultsHovered Bool | BlurReceiverField | Noop type alias Model = { receivers : List Receiver , matches : List Receiver , fieldText : String , selectedReceiver : Maybe Receiver , showReceivers : Bool , resultsHovered : Bool } initReceiverBar : Model initReceiverBar = { receivers = [] , matches = [] , fieldText = "" , selectedReceiver = Nothing , showReceivers = False , resultsHovered = False } prometheus-alertmanager-0.15.3+ds/ui/app/src/Views/ReceiverBar/Updates.elm000066400000000000000000000050131341674552200264230ustar00rootroot00000000000000module Views.ReceiverBar.Updates exposing (update, fetchReceivers) import Views.ReceiverBar.Types exposing (Model, Msg(..)) import Utils.Types exposing (ApiData(Success)) import Utils.Filter exposing (Filter, generateQueryString, stringifyGroup, parseGroup) import Navigation import Dom import Task import Alerts.Api as Api import Utils.Match exposing (jaroWinkler) update : String -> Filter -> Msg -> Model -> ( Model, Cmd Msg ) update url filter msg model = case msg of ReceiversFetched (Success receivers) -> ( { model | receivers = receivers }, Cmd.none ) ReceiversFetched _ -> ( model, Cmd.none ) EditReceivers -> ( { model | showReceivers = True , fieldText = "" , matches = model.receivers |> List.take 10 |> (::) { name = "All", regex = "" } , selectedReceiver = Nothing } , Dom.focus "receiver-field" |> Task.attempt (always Noop) ) ResultsHovered resultsHovered -> ( { model | resultsHovered = resultsHovered }, Cmd.none ) UpdateReceiver receiver -> let matches = model.receivers |> List.sortBy (.name >> jaroWinkler receiver) |> List.reverse |> List.take 10 |> (::) { name = "All", regex = "" } in ( { model | fieldText = receiver , matches = matches } , Cmd.none ) BlurReceiverField -> ( { model | showReceivers = False }, Cmd.none ) Select maybeReceiver -> ( { model | selectedReceiver = maybeReceiver }, Cmd.none ) FilterByReceiver regex -> ( { model | showReceivers = False, resultsHovered = False } , Navigation.newUrl (url ++ generateQueryString { filter | receiver = if regex == "" then Nothing else Just regex } ) ) Noop -> ( model, Cmd.none ) fetchReceivers : String -> Cmd Msg fetchReceivers = Api.fetchReceivers >> Cmd.map ReceiversFetched prometheus-alertmanager-0.15.3+ds/ui/app/src/Views/ReceiverBar/Views.elm000066400000000000000000000071331341674552200261200ustar00rootroot00000000000000module Views.ReceiverBar.Views exposing (view) import Html exposing (Html, li, div, text, input) import Html.Attributes exposing (class, style, tabindex, value, id) import Html.Events exposing (onBlur, onClick, onInput, onMouseEnter, onMouseLeave) import Views.ReceiverBar.Types exposing (Model, Msg(..)) import Alerts.Types exposing (Receiver) import Utils.Keyboard exposing (keys, onKeyUp, onKeyDown) import Utils.List view : Maybe String -> Model -> Html Msg view maybeRegex model = if model.showReceivers || model.resultsHovered then viewDropdown model else viewResult maybeRegex model.receivers viewResult : Maybe String -> List Receiver -> Html Msg viewResult maybeRegex receivers = let unescapedReceiver = receivers |> List.filter (.regex >> Just >> (==) maybeRegex) |> List.map (.name >> Just) |> List.head |> Maybe.withDefault maybeRegex in li [ class ("nav-item ml-auto") , tabindex 1 , style [ ( "position", "relative" ) , ( "outline", "none" ) ] ] [ div [ onClick EditReceivers , class "mt-1 mr-4" , style [ ( "cursor", "pointer" ) ] ] [ text ("Receiver: " ++ Maybe.withDefault "All" unescapedReceiver) ] ] viewDropdown : Model -> Html Msg viewDropdown { matches, fieldText, selectedReceiver } = let nextMatch = selectedReceiver |> Maybe.map (flip Utils.List.nextElem <| matches) |> Maybe.withDefault (List.head matches) prevMatch = selectedReceiver |> Maybe.map (flip Utils.List.nextElem <| List.reverse matches) |> Maybe.withDefault (Utils.List.lastElem matches) keyDown key = if key == keys.down then Select nextMatch else if key == keys.up then Select prevMatch else if key == keys.enter then selectedReceiver |> Maybe.map .regex |> Maybe.withDefault fieldText |> FilterByReceiver else Noop in li [ class ("nav-item ml-auto mr-4 autocomplete-menu show") , onMouseEnter (ResultsHovered True) , onMouseLeave (ResultsHovered False) , style [ ( "position", "relative" ) , ( "outline", "none" ) ] ] [ input [ id "receiver-field" , value fieldText , onBlur BlurReceiverField , onInput UpdateReceiver , onKeyDown keyDown , class "mr-4" , style [ ( "display", "block" ) , ( "width", "100%" ) ] ] [] , matches |> List.map (receiverField selectedReceiver) |> div [ class "dropdown-menu dropdown-menu-right" ] ] receiverField : Maybe Receiver -> Receiver -> Html Msg receiverField selected receiver = let attrs = if selected == Just receiver then [ class "dropdown-item active" ] else [ class "dropdown-item" , style [ ( "cursor", "pointer" ) ] , onClick (FilterByReceiver receiver.regex) ] in div attrs [ text receiver.name ] prometheus-alertmanager-0.15.3+ds/ui/app/src/Views/Shared/000077500000000000000000000000001341674552200233355ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/ui/app/src/Views/Shared/AlertCompact.elm000066400000000000000000000006751341674552200264220ustar00rootroot00000000000000module Views.Shared.AlertCompact exposing (view) import Alerts.Types exposing (Alert) import Html exposing (Html, span, div, text, li) import Html.Attributes exposing (class) import Utils.Views exposing (labelButton) view : Alert -> Html msg view alert = li [ class "mb2 w-80-l w-100-m" ] <| List.map (\( key, value ) -> labelButton Nothing (key ++ "=" ++ value) ) alert.labels prometheus-alertmanager-0.15.3+ds/ui/app/src/Views/Shared/AlertListCompact.elm000066400000000000000000000005161341674552200272500ustar00rootroot00000000000000module Views.Shared.AlertListCompact exposing (view) import Alerts.Types exposing (Alert) import Html exposing (Html, ol) import Html.Attributes exposing (class) import Views.Shared.AlertCompact view : List Alert -> Html msg view alerts = List.map Views.Shared.AlertCompact.view alerts |> ol [ class "list pa0 w-100" ] prometheus-alertmanager-0.15.3+ds/ui/app/src/Views/Shared/SilencePreview.elm000066400000000000000000000017531341674552200267660ustar00rootroot00000000000000module Views.Shared.SilencePreview exposing (view) import Alerts.Types exposing (Alert) import Html exposing (Html, div, p, strong, text) import Html.Attributes exposing (class) import Utils.Types exposing (ApiData(Failure, Initial, Loading, Success)) import Utils.Views exposing (loading) import Views.Shared.AlertListCompact view : ApiData (List Alert) -> Html msg view alertsResponse = case alertsResponse of Success alerts -> if List.isEmpty alerts then div [ class "w-100" ] [ p [] [ strong [] [ text "No silenced alerts" ] ] ] else div [ class "w-100" ] [ p [] [ strong [] [ text ("Silenced alerts: " ++ toString (List.length alerts)) ] ] , Views.Shared.AlertListCompact.view alerts ] Initial -> text "" Loading -> loading Failure e -> div [ class "alert alert-warning" ] [ text e ] prometheus-alertmanager-0.15.3+ds/ui/app/src/Views/SilenceForm/000077500000000000000000000000001341674552200243355ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/ui/app/src/Views/SilenceForm/Parsing.elm000066400000000000000000000016061341674552200264420ustar00rootroot00000000000000module Views.SilenceForm.Parsing exposing (newSilenceFromAlertLabels, silenceFormNewParser, silenceFormEditParser) import UrlParser exposing (Parser, s, (), (), string, stringParam, oneOf, map) import Utils.Filter exposing (parseFilter, Matcher) import Http exposing (encodeUri) newSilenceFromAlertLabels : List ( String, String ) -> String newSilenceFromAlertLabels labels = labels |> List.map (\( k, v ) -> Utils.Filter.Matcher k Utils.Filter.Eq v) |> Utils.Filter.stringifyFilter |> encodeUri |> (++) "#/silences/new?filter=" silenceFormNewParser : Parser (List Matcher -> a) a silenceFormNewParser = s "silences" s "new" stringParam "filter" |> map (Maybe.andThen parseFilter >> Maybe.withDefault []) silenceFormEditParser : Parser (String -> a) a silenceFormEditParser = s "silences" string s "edit" prometheus-alertmanager-0.15.3+ds/ui/app/src/Views/SilenceForm/Types.elm000066400000000000000000000144161341674552200261460ustar00rootroot00000000000000module Views.SilenceForm.Types exposing ( SilenceFormMsg(..) , SilenceFormFieldMsg(..) , Model , SilenceForm , MatcherForm , fromMatchersAndTime , fromSilence , toSilence , initSilenceForm , emptyMatcher , validateForm , parseEndsAt ) import Silences.Types exposing (Silence, SilenceId, nullSilence) import Alerts.Types exposing (Alert) import Utils.Types exposing (Matcher, Duration, ApiData(..)) import Time exposing (Time) import Utils.Date exposing (timeFromString, timeToString, durationFormat, parseDuration) import Time exposing (Time) import Utils.Filter import Utils.FormValidation exposing ( initialField , ValidationState(..) , ValidatedField , validate , stringNotEmpty ) type alias Model = { form : SilenceForm , silenceId : ApiData String , alerts : ApiData (List Alert) } type alias SilenceForm = { id : String , createdBy : ValidatedField , comment : ValidatedField , startsAt : ValidatedField , endsAt : ValidatedField , duration : ValidatedField , matchers : List MatcherForm } type alias MatcherForm = { name : ValidatedField , value : ValidatedField , isRegex : Bool } type SilenceFormMsg = UpdateField SilenceFormFieldMsg | CreateSilence | PreviewSilence | AlertGroupsPreview (ApiData (List Alert)) | FetchSilence String | NewSilenceFromMatchers String (List Utils.Filter.Matcher) | NewSilenceFromMatchersAndTime String (List Utils.Filter.Matcher) Time | SilenceFetch (ApiData Silence) | SilenceCreate (ApiData SilenceId) type SilenceFormFieldMsg = AddMatcher | DeleteMatcher Int | UpdateStartsAt String | UpdateEndsAt String | UpdateDuration String | ValidateTime | UpdateCreatedBy String | ValidateCreatedBy | UpdateComment String | ValidateComment | UpdateMatcherName Int String | ValidateMatcherName Int | UpdateMatcherValue Int String | ValidateMatcherValue Int | UpdateMatcherRegex Int Bool initSilenceForm : Model initSilenceForm = { form = empty , silenceId = Utils.Types.Initial , alerts = Utils.Types.Initial } toSilence : SilenceForm -> Maybe Silence toSilence { id, comment, matchers, createdBy, startsAt, endsAt } = Result.map5 (\nonEmptyComment validMatchers nonEmptyCreatedBy parsedStartsAt parsedEndsAt -> { nullSilence | id = id , comment = nonEmptyComment , matchers = validMatchers , createdBy = nonEmptyCreatedBy , startsAt = parsedStartsAt , endsAt = parsedEndsAt } ) (stringNotEmpty comment.value) (List.foldr appendMatcher (Ok []) matchers) (stringNotEmpty createdBy.value) (timeFromString startsAt.value) (parseEndsAt startsAt.value endsAt.value) |> Result.toMaybe fromSilence : Silence -> SilenceForm fromSilence { id, createdBy, comment, startsAt, endsAt, matchers } = { id = id , createdBy = initialField createdBy , comment = initialField comment , startsAt = initialField (timeToString startsAt) , endsAt = initialField (timeToString endsAt) , duration = initialField (durationFormat (endsAt - startsAt) |> Maybe.withDefault "") , matchers = List.map fromMatcher matchers } validateForm : SilenceForm -> SilenceForm validateForm { id, createdBy, comment, startsAt, endsAt, duration, matchers } = { id = id , createdBy = validate stringNotEmpty createdBy , comment = validate stringNotEmpty comment , startsAt = validate timeFromString startsAt , endsAt = validate (parseEndsAt startsAt.value) endsAt , duration = validate parseDuration duration , matchers = List.map validateMatcherForm matchers } parseEndsAt : String -> String -> Result String Time.Time parseEndsAt startsAt endsAt = case ( timeFromString startsAt, timeFromString endsAt ) of ( Ok starts, Ok ends ) -> if starts > ends then Err "Can't be in the past" else Ok ends ( _, endsResult ) -> endsResult validateMatcherForm : MatcherForm -> MatcherForm validateMatcherForm { name, value, isRegex } = { name = validate stringNotEmpty name , value = value , isRegex = isRegex } empty : SilenceForm empty = { id = "" , createdBy = initialField "" , comment = initialField "" , startsAt = initialField "" , endsAt = initialField "" , duration = initialField "" , matchers = [] } emptyMatcher : MatcherForm emptyMatcher = { isRegex = False , name = initialField "" , value = initialField "" } defaultDuration : Time defaultDuration = 2 * Time.hour fromMatchersAndTime : String -> List Utils.Filter.Matcher -> Time -> SilenceForm fromMatchersAndTime defaultCreator matchers now = { empty | startsAt = initialField (timeToString now) , endsAt = initialField (timeToString (now + defaultDuration)) , duration = initialField (durationFormat defaultDuration |> Maybe.withDefault "") , createdBy = initialField defaultCreator , matchers = -- If no matchers were specified, add an empty row if List.isEmpty matchers then [ emptyMatcher ] else List.filterMap (filterMatcherToMatcher >> Maybe.map fromMatcher) matchers } appendMatcher : MatcherForm -> Result String (List Matcher) -> Result String (List Matcher) appendMatcher { isRegex, name, value } = Result.map2 (::) (Result.map2 (Matcher isRegex) (stringNotEmpty name.value) (Ok value.value)) filterMatcherToMatcher : Utils.Filter.Matcher -> Maybe Matcher filterMatcherToMatcher { key, op, value } = Maybe.map (\op -> Matcher op key value) <| case op of Utils.Filter.Eq -> Just False Utils.Filter.RegexMatch -> Just True -- we don't support negative matchers _ -> Nothing fromMatcher : Matcher -> MatcherForm fromMatcher { name, value, isRegex } = { name = initialField name , value = initialField value , isRegex = isRegex } prometheus-alertmanager-0.15.3+ds/ui/app/src/Views/SilenceForm/Updates.elm000066400000000000000000000214571341674552200264520ustar00rootroot00000000000000port module Views.SilenceForm.Updates exposing (update) import Alerts.Api import Silences.Api import Task import Time import Types exposing (Msg(MsgForSilenceForm, SetDefaultCreator)) import Navigation import Utils.Date exposing (timeFromString) import Utils.List import Utils.Types exposing (ApiData(..)) import Utils.Filter exposing (nullFilter) import Utils.FormValidation exposing (updateValue, validate, stringNotEmpty, fromResult) import Views.SilenceForm.Types exposing ( Model , SilenceForm , SilenceFormMsg(..) , SilenceFormFieldMsg(..) , fromMatchersAndTime , fromSilence , parseEndsAt , validateForm , toSilence , emptyMatcher ) updateForm : SilenceFormFieldMsg -> SilenceForm -> SilenceForm updateForm msg form = case msg of AddMatcher -> { form | matchers = form.matchers ++ [ emptyMatcher ] } UpdateStartsAt time -> let startsAt = Utils.Date.timeFromString time endsAt = Utils.Date.timeFromString form.endsAt.value durationValue = case Result.map2 (-) endsAt startsAt of Ok duration -> case Utils.Date.durationFormat duration of Just value -> value Nothing -> form.duration.value Err _ -> form.duration.value in { form | startsAt = updateValue time form.startsAt , duration = updateValue durationValue form.duration } UpdateEndsAt time -> let endsAt = Utils.Date.timeFromString time startsAt = Utils.Date.timeFromString form.startsAt.value durationValue = case Result.map2 (-) endsAt startsAt of Ok duration -> case Utils.Date.durationFormat duration of Just value -> value Nothing -> form.duration.value Err _ -> form.duration.value in { form | endsAt = updateValue time form.endsAt , duration = updateValue durationValue form.duration } UpdateDuration time -> let duration = Utils.Date.parseDuration time startsAt = Utils.Date.timeFromString form.startsAt.value endsAtValue = case Result.map2 (+) startsAt duration of Ok endsAt -> Utils.Date.timeToString endsAt Err _ -> form.endsAt.value in { form | endsAt = updateValue endsAtValue form.endsAt , duration = updateValue time form.duration } ValidateTime -> { form | startsAt = validate Utils.Date.timeFromString form.startsAt , endsAt = validate (parseEndsAt form.startsAt.value) form.endsAt , duration = validate Utils.Date.parseDuration form.duration } UpdateCreatedBy createdBy -> { form | createdBy = updateValue createdBy form.createdBy } ValidateCreatedBy -> { form | createdBy = validate stringNotEmpty form.createdBy } UpdateComment comment -> { form | comment = updateValue comment form.comment } ValidateComment -> { form | comment = validate stringNotEmpty form.comment } DeleteMatcher index -> { form | matchers = List.take index form.matchers ++ List.drop (index + 1) form.matchers } UpdateMatcherName index name -> let matchers = Utils.List.replaceIndex index (\matcher -> { matcher | name = updateValue name matcher.name }) form.matchers in { form | matchers = matchers } ValidateMatcherName index -> let matchers = Utils.List.replaceIndex index (\matcher -> { matcher | name = validate stringNotEmpty matcher.name }) form.matchers in { form | matchers = matchers } UpdateMatcherValue index value -> let matchers = Utils.List.replaceIndex index (\matcher -> { matcher | value = updateValue value matcher.value }) form.matchers in { form | matchers = matchers } ValidateMatcherValue index -> let matchers = Utils.List.replaceIndex index (\matcher -> { matcher | value = matcher.value }) form.matchers in { form | matchers = matchers } UpdateMatcherRegex index isRegex -> let matchers = Utils.List.replaceIndex index (\matcher -> { matcher | isRegex = isRegex }) form.matchers in { form | matchers = matchers } update : SilenceFormMsg -> Model -> String -> String -> ( Model, Cmd Msg ) update msg model basePath apiUrl = case msg of CreateSilence -> case toSilence model.form of Just silence -> ( { model | silenceId = Loading } , Cmd.batch [ Silences.Api.create apiUrl silence |> Cmd.map (SilenceCreate >> MsgForSilenceForm) , persistDefaultCreator silence.createdBy , Task.succeed silence.createdBy |> Task.perform SetDefaultCreator ] ) Nothing -> ( { model | silenceId = Failure "Could not submit the form, Silence is not yet valid." , form = validateForm model.form } , Cmd.none ) SilenceCreate silenceId -> let cmd = case silenceId of Success id -> Navigation.newUrl (basePath ++ "#/silences/" ++ id) _ -> Cmd.none in ( { model | silenceId = silenceId }, cmd ) NewSilenceFromMatchers defaultCreator matchers -> ( model, Task.perform (NewSilenceFromMatchersAndTime defaultCreator matchers >> MsgForSilenceForm) Time.now ) NewSilenceFromMatchersAndTime defaultCreator matchers time -> ( { form = fromMatchersAndTime defaultCreator matchers time , alerts = Initial , silenceId = Initial } , Cmd.none ) FetchSilence silenceId -> ( model, Silences.Api.getSilence apiUrl silenceId (SilenceFetch >> MsgForSilenceForm) ) SilenceFetch (Success silence) -> ( { model | form = fromSilence silence } , Task.perform identity (Task.succeed (MsgForSilenceForm PreviewSilence)) ) SilenceFetch _ -> ( model, Cmd.none ) PreviewSilence -> case toSilence model.form of Just silence -> ( { model | alerts = Loading } , Alerts.Api.fetchAlerts apiUrl { nullFilter | text = Just (Utils.List.mjoin silence.matchers) } |> Cmd.map (AlertGroupsPreview >> MsgForSilenceForm) ) Nothing -> ( { model | alerts = Failure "Can not display affected Alerts, Silence is not yet valid." , form = validateForm model.form } , Cmd.none ) AlertGroupsPreview alerts -> ( { model | alerts = alerts } , Cmd.none ) UpdateField fieldMsg -> ( { form = updateForm fieldMsg model.form , alerts = Initial , silenceId = Initial } , Cmd.none ) port persistDefaultCreator : String -> Cmd msg prometheus-alertmanager-0.15.3+ds/ui/app/src/Views/SilenceForm/Views.elm000066400000000000000000000127141341674552200261360ustar00rootroot00000000000000module Views.SilenceForm.Views exposing (view) import Html exposing (Html, a, div, fieldset, label, legend, span, text, h1, strong, button, input, textarea) import Html.Attributes exposing (class, href) import Html.Events exposing (onClick) import Silences.Types exposing (Silence, SilenceId) import Alerts.Types exposing (Alert) import Views.Shared.SilencePreview import Views.SilenceForm.Types exposing (Model, SilenceFormMsg(..), MatcherForm) import Utils.Types exposing (ApiData) import Utils.Views exposing (checkbox, iconButtonMsg, validatedField, loading) import Utils.FormValidation exposing (ValidationState(..), ValidatedField) import Views.SilenceForm.Types exposing (Model, SilenceFormMsg(..), SilenceFormFieldMsg(..), SilenceForm) import Utils.Filter view : Maybe SilenceId -> List Utils.Filter.Matcher -> String -> Model -> Html SilenceFormMsg view maybeId matchers defaultCreator { form, silenceId, alerts } = let ( title, resetClick ) = case maybeId of Just silenceId -> ( "Edit Silence", FetchSilence silenceId ) Nothing -> ( "New Silence", NewSilenceFromMatchers defaultCreator matchers ) in div [] [ h1 [] [ text title ] , timeInput form.startsAt form.endsAt form.duration , matcherInput form.matchers , validatedField input "Creator" inputSectionPadding (UpdateCreatedBy >> UpdateField) (ValidateCreatedBy |> UpdateField) form.createdBy , validatedField textarea "Comment" inputSectionPadding (UpdateComment >> UpdateField) (ValidateComment |> UpdateField) form.comment , div [ class inputSectionPadding ] [ informationBlock silenceId alerts , silenceActionButtons maybeId form resetClick ] ] inputSectionPadding : String inputSectionPadding = "mt-5" timeInput : ValidatedField -> ValidatedField -> ValidatedField -> Html SilenceFormMsg timeInput startsAt endsAt duration = div [ class <| "row " ++ inputSectionPadding ] [ validatedField input "Start" "col-5" (UpdateStartsAt >> UpdateField) (ValidateTime |> UpdateField) startsAt , validatedField input "Duration" "col-2" (UpdateDuration >> UpdateField) (ValidateTime |> UpdateField) duration , validatedField input "End" "col-5" (UpdateEndsAt >> UpdateField) (ValidateTime |> UpdateField) endsAt ] matcherInput : List MatcherForm -> Html SilenceFormMsg matcherInput matchers = div [ class inputSectionPadding ] [ div [] [ label [] [ strong [] [ text "Matchers " ] , span [ class "" ] [ text "Alerts affected by this silence." ] ] , div [ class "row" ] [ label [ class "col-5" ] [ text "Name" ] , label [ class "col-5" ] [ text "Value" ] ] ] , div [] (List.indexedMap (matcherForm (List.length matchers > 1)) matchers) , iconButtonMsg "btn btn-secondary" "fa-plus" (AddMatcher |> UpdateField) ] informationBlock : ApiData SilenceId -> ApiData (List Alert) -> Html SilenceFormMsg informationBlock silence alerts = case silence of Utils.Types.Success _ -> text "" Utils.Types.Initial -> Views.Shared.SilencePreview.view alerts Utils.Types.Failure error -> Utils.Views.error error Utils.Types.Loading -> loading silenceActionButtons : Maybe String -> SilenceForm -> SilenceFormMsg -> Html SilenceFormMsg silenceActionButtons maybeId form resetClick = div [ class ("mb-4 " ++ inputSectionPadding) ] [ previewSilenceBtn , createSilenceBtn maybeId , button [ class "ml-2 btn btn-danger", onClick resetClick ] [ text "Reset" ] ] createSilenceBtn : Maybe String -> Html SilenceFormMsg createSilenceBtn maybeId = let btnTxt = case maybeId of Just _ -> "Update" Nothing -> "Create" in button [ class "ml-2 btn btn-primary" , onClick CreateSilence ] [ text btnTxt ] previewSilenceBtn : Html SilenceFormMsg previewSilenceBtn = button [ class "btn btn-outline-success" , onClick PreviewSilence ] [ text "Preview Alerts" ] matcherForm : Bool -> Int -> MatcherForm -> Html SilenceFormMsg matcherForm showDeleteButton index { name, value, isRegex } = div [ class "row" ] [ div [ class "col-5" ] [ validatedField input "" "" (UpdateMatcherName index) (ValidateMatcherName index) name ] , div [ class "col-5" ] [ validatedField input "" "" (UpdateMatcherValue index) (ValidateMatcherValue index) value ] , div [ class "col-2 d-flex align-items-center" ] [ checkbox "Regex" isRegex (UpdateMatcherRegex index) , if showDeleteButton then iconButtonMsg "btn btn-secondary ml-auto" "fa-trash-o" (DeleteMatcher index) else text "" ] ] |> Html.map UpdateField prometheus-alertmanager-0.15.3+ds/ui/app/src/Views/SilenceList/000077500000000000000000000000001341674552200243455ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/ui/app/src/Views/SilenceList/Parsing.elm000066400000000000000000000005531341674552200264520ustar00rootroot00000000000000module Views.SilenceList.Parsing exposing (silenceListParser) import UrlParser exposing ((), Parser, s, stringParam, map) import Utils.Filter exposing (Filter) silenceListParser : Parser (Filter -> a) a silenceListParser = map (\t -> Filter t Nothing Nothing Nothing Nothing ) (s "silences" stringParam "filter") prometheus-alertmanager-0.15.3+ds/ui/app/src/Views/SilenceList/SilenceView.elm000066400000000000000000000116061341674552200272650ustar00rootroot00000000000000module Views.SilenceList.SilenceView exposing (deleteButton, editButton, view) import Dialog import Html exposing (Html, a, b, button, div, h3, i, li, p, small, span, text) import Html.Attributes exposing (class, href, style) import Html.Events exposing (onClick) import Silences.Types exposing (Silence, State(Active, Expired, Pending)) import Time exposing (Time) import Types exposing (Msg(MsgForSilenceForm, MsgForSilenceList, Noop)) import Utils.Date import Utils.Filter import Utils.List import Utils.Types exposing (Matcher) import Utils.Views exposing (buttonLink) import Views.FilterBar.Types as FilterBarTypes import Views.SilenceList.Types exposing (SilenceListMsg(ConfirmDestroySilence, DestroySilence, FetchSilences, MsgForFilterBar)) import Views.SilenceForm.Parsing exposing (newSilenceFromAlertLabels) view : Bool -> Silence -> Html Msg view showConfirmationDialog silence = li [ -- speedup rendering in Chrome, because list-group-item className -- creates a new layer in the rendering engine style [ ( "position", "static" ) ] , class "align-items-start list-group-item border-0 p-0 mb-4" ] [ div [ class "w-100 mb-2 d-flex align-items-start" ] [ case silence.status.state of Active -> dateView "Ends" silence.endsAt Pending -> dateView "Starts" silence.startsAt Expired -> dateView "Expired" silence.endsAt , detailsButton silence.id , editButton silence , deleteButton silence False ] , div [ class "" ] (List.map matcherButton silence.matchers) , Dialog.view (if showConfirmationDialog then Just (confirmSilenceDeleteView silence False) else Nothing ) ] confirmSilenceDeleteView : Silence -> Bool -> Dialog.Config Msg confirmSilenceDeleteView silence refresh = { closeMessage = Just (MsgForSilenceList Views.SilenceList.Types.FetchSilences) , containerClass = Nothing , header = Just (h3 [] [ text "Expire Silence" ]) , body = Just (text "Are you sure you want to expire this silence?") , footer = Just (button [ class "btn btn-success" , onClick (MsgForSilenceList (Views.SilenceList.Types.DestroySilence silence refresh)) ] [ text "Confirm" ] ) } dateView : String -> Time -> Html Msg dateView string time = span [ class "text-muted align-self-center mr-2" ] [ text (string ++ " " ++ Utils.Date.timeFormat time ++ ", " ++ Utils.Date.dateFormat time) ] matcherButton : Matcher -> Html Msg matcherButton matcher = let op = if matcher.isRegex then Utils.Filter.RegexMatch else Utils.Filter.Eq msg = FilterBarTypes.AddFilterMatcher False { key = matcher.name , op = op , value = matcher.value } |> MsgForFilterBar |> MsgForSilenceList in Utils.Views.labelButton (Just msg) (Utils.List.mstring matcher) editButton : Silence -> Html Msg editButton silence = let matchers = List.map (\s -> ( s.name, s.value )) silence.matchers in case silence.status.state of -- If the silence is expired, do not edit it, but instead create a new -- one with the old matchers Expired -> a [ class "btn btn-outline-info border-0" , href (newSilenceFromAlertLabels matchers) ] [ text "Recreate" ] _ -> let editUrl = String.join "/" [ "#/silences", silence.id, "edit" ] in a [ class "btn btn-outline-info border-0", href editUrl ] [ text "Edit" ] deleteButton : Silence -> Bool -> Html Msg deleteButton silence refresh = case silence.status.state of Expired -> text "" Active -> button [ class "btn btn-outline-danger border-0" , onClick (MsgForSilenceList (ConfirmDestroySilence silence refresh)) ] [ text "Expire" ] Pending -> button [ class "btn btn-outline-danger border-0" , onClick (MsgForSilenceList (ConfirmDestroySilence silence refresh)) ] [ text "Delete" ] detailsButton : String -> Html Msg detailsButton silenceId = a [ class "btn btn-outline-info border-0", href ("#/silences/" ++ silenceId) ] [ text "View" ] prometheus-alertmanager-0.15.3+ds/ui/app/src/Views/SilenceList/Types.elm000066400000000000000000000016211341674552200261500ustar00rootroot00000000000000module Views.SilenceList.Types exposing (Model, SilenceTab, SilenceListMsg(..), initSilenceList) import Silences.Types exposing (Silence, State(Active), SilenceId) import Utils.Types exposing (ApiData(Initial)) import Views.FilterBar.Types as FilterBar type SilenceListMsg = ConfirmDestroySilence Silence Bool | DestroySilence Silence Bool | SilencesFetch (ApiData (List Silence)) | FetchSilences | MsgForFilterBar FilterBar.Msg | SetTab State type alias SilenceTab = { silences : List Silence , tab : State , count : Int } type alias Model = { silences : ApiData (List SilenceTab) , filterBar : FilterBar.Model , tab : State , showConfirmationDialog : Maybe SilenceId } initSilenceList : Model initSilenceList = { silences = Initial , filterBar = FilterBar.initFilterBar , tab = Active , showConfirmationDialog = Nothing } prometheus-alertmanager-0.15.3+ds/ui/app/src/Views/SilenceList/Updates.elm000066400000000000000000000057371341674552200264650ustar00rootroot00000000000000module Views.SilenceList.Updates exposing (update, urlUpdate) import Navigation import Silences.Api as Api import Utils.Api as ApiData import Utils.Filter exposing (Filter, generateQueryString) import Utils.Types as Types exposing (ApiData(Failure, Loading, Success), Matchers, Time) import Views.FilterBar.Updates as FilterBar import Views.SilenceList.Types exposing (Model, SilenceTab, SilenceListMsg(..)) import Silences.Types exposing (Silence, State(..)) update : SilenceListMsg -> Model -> Filter -> String -> String -> ( Model, Cmd SilenceListMsg ) update msg model filter basePath apiUrl = case msg of SilencesFetch fetchedSilences -> ( { model | silences = ApiData.map (\silences -> List.map (groupSilencesByState silences) states) fetchedSilences } , Cmd.none ) FetchSilences -> ( { model | filterBar = FilterBar.setMatchers filter model.filterBar , silences = Loading , showConfirmationDialog = Nothing } , Api.getSilences apiUrl filter SilencesFetch ) ConfirmDestroySilence silence refresh -> ( { model | showConfirmationDialog = Just silence.id } , Cmd.none ) DestroySilence silence refresh -> -- TODO: "Deleted id: ID" growl -- TODO: Check why POST isn't there but is accepted { model | silences = Loading, showConfirmationDialog = Nothing } ! [ Api.destroy apiUrl silence (always FetchSilences) , if refresh then Navigation.newUrl (basePath ++ "#/silences") else Cmd.none ] MsgForFilterBar msg -> let ( filterBar, cmd ) = FilterBar.update (basePath ++ "#/silences") filter msg model.filterBar in ( { model | filterBar = filterBar }, Cmd.map MsgForFilterBar cmd ) SetTab tab -> ( { model | tab = tab }, Cmd.none ) groupSilencesByState : List Silence -> State -> SilenceTab groupSilencesByState silences state = let silencesInTab = filterSilencesByState state silences in { tab = state , silences = silencesInTab , count = List.length silencesInTab } states : List State states = [ Active, Pending, Expired ] filterSilencesByState : State -> List Silence -> List Silence filterSilencesByState state = List.filter (.status >> .state >> (==) state) urlUpdate : Maybe String -> ( SilenceListMsg, Filter ) urlUpdate maybeString = ( FetchSilences, updateFilter maybeString ) updateFilter : Maybe String -> Filter updateFilter maybeFilter = { receiver = Nothing , showSilenced = Nothing , showInhibited = Nothing , group = Nothing , text = maybeFilter } prometheus-alertmanager-0.15.3+ds/ui/app/src/Views/SilenceList/Views.elm000066400000000000000000000070561341674552200261510ustar00rootroot00000000000000module Views.SilenceList.Views exposing (..) import Html exposing (..) import Html.Attributes exposing (..) import Silences.Types exposing (Silence, State(..), stateToString, SilenceId) import Types exposing (Msg(MsgForSilenceList, Noop, UpdateFilter)) import Utils.String as StringUtils import Utils.Types exposing (ApiData(..), Matcher) import Utils.Views exposing (buttonLink, checkbox, error, formField, formInput, iconButtonMsg, loading, textField) import Views.FilterBar.Views as FilterBar import Views.SilenceList.SilenceView import Views.SilenceList.Types exposing (Model, SilenceListMsg(..), SilenceTab) import Html.Lazy exposing (lazy, lazy2, lazy3) import Html.Keyed view : Model -> Html Msg view { filterBar, tab, silences, showConfirmationDialog } = div [] [ div [ class "mb-4" ] [ label [ class "mb-2", for "filter-bar-matcher" ] [ text "Filter" ] , Html.map (MsgForFilterBar >> MsgForSilenceList) (FilterBar.view filterBar) ] , lazy2 tabsView tab silences , lazy3 silencesView showConfirmationDialog tab silences ] tabsView : State -> ApiData (List SilenceTab) -> Html Msg tabsView currentTab tabs = case tabs of Success silencesTabs -> List.map (\{ tab, count } -> tabView currentTab count tab) silencesTabs |> ul [ class "nav nav-tabs mb-4" ] _ -> List.map (tabView currentTab 0) states |> ul [ class "nav nav-tabs mb-4" ] tabView : State -> Int -> State -> Html Msg tabView currentTab count tab = Utils.Views.tab tab currentTab (SetTab >> MsgForSilenceList) <| case count of 0 -> [ text (StringUtils.capitalizeFirst (stateToString tab)) ] n -> [ text (StringUtils.capitalizeFirst (stateToString tab)) , span [ class "badge badge-pillow badge-default align-text-top ml-2" ] [ text (toString n) ] ] silencesView : Maybe SilenceId -> State -> ApiData (List SilenceTab) -> Html Msg silencesView showConfirmationDialog tab silencesTab = case silencesTab of Success tabs -> tabs |> List.filter (.tab >> (==) tab) |> List.head |> Maybe.map .silences |> Maybe.withDefault [] |> (\silences -> if List.isEmpty silences then Utils.Views.error "No silences found" else Html.Keyed.ul [ class "list-group" ] (List.map (\silence -> ( silence.id , Views.SilenceList.SilenceView.view (showConfirmationDialog == Just silence.id) silence ) ) silences ) ) Failure msg -> error msg _ -> loading groupSilencesByState : List Silence -> List ( State, List Silence ) groupSilencesByState silences = List.map (\state -> ( state, filterSilencesByState state silences )) states states : List State states = [ Active, Pending, Expired ] filterSilencesByState : State -> List Silence -> List Silence filterSilencesByState state = List.filter (.status >> .state >> (==) state) prometheus-alertmanager-0.15.3+ds/ui/app/src/Views/SilenceView/000077500000000000000000000000001341674552200243445ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/ui/app/src/Views/SilenceView/Parsing.elm000066400000000000000000000003211341674552200264420ustar00rootroot00000000000000module Views.SilenceView.Parsing exposing (silenceViewParser) import UrlParser exposing (Parser, s, string, ()) silenceViewParser : Parser (String -> a) a silenceViewParser = s "silences" string prometheus-alertmanager-0.15.3+ds/ui/app/src/Views/SilenceView/Types.elm000066400000000000000000000013131341674552200261450ustar00rootroot00000000000000module Views.SilenceView.Types exposing (Model, SilenceViewMsg(..), initSilenceView) import Alerts.Types exposing (Alert) import Silences.Types exposing (Silence, SilenceId) import Utils.Types exposing (ApiData(Initial)) type SilenceViewMsg = FetchSilence String | SilenceFetched (ApiData Silence) | AlertGroupsPreview (ApiData (List Alert)) | InitSilenceView SilenceId | ConfirmDestroySilence Silence Bool | Reload String type alias Model = { silence : ApiData Silence , alerts : ApiData (List Alert) , showConfirmationDialog : Bool } initSilenceView : Model initSilenceView = { silence = Initial , alerts = Initial , showConfirmationDialog = False } prometheus-alertmanager-0.15.3+ds/ui/app/src/Views/SilenceView/Updates.elm000066400000000000000000000030141341674552200264460ustar00rootroot00000000000000module Views.SilenceView.Updates exposing (update) import Alerts.Api import Navigation exposing (newUrl) import Silences.Api exposing (getSilence) import Utils.Filter exposing (nullFilter) import Utils.List import Utils.Types exposing (ApiData(..)) import Views.SilenceView.Types exposing (Model, SilenceViewMsg(..)) update : SilenceViewMsg -> Model -> String -> ( Model, Cmd SilenceViewMsg ) update msg model apiUrl = case msg of FetchSilence id -> ( model, getSilence apiUrl id SilenceFetched ) AlertGroupsPreview alerts -> ( { model | alerts = alerts } , Cmd.none ) SilenceFetched (Success silence) -> ( { model | silence = Success silence , alerts = Loading } , Alerts.Api.fetchAlerts apiUrl { nullFilter | text = Just (Utils.List.mjoin silence.matchers), showSilenced = Just True } |> Cmd.map AlertGroupsPreview ) ConfirmDestroySilence silence refresh -> ( { model | showConfirmationDialog = True } , Cmd.none ) SilenceFetched silence -> ( { model | silence = silence, alerts = Initial }, Cmd.none ) InitSilenceView silenceId -> ( { model | showConfirmationDialog = False }, getSilence apiUrl silenceId SilenceFetched ) Reload silenceId -> ( { model | showConfirmationDialog = False }, newUrl ("#/silences/" ++ silenceId) ) prometheus-alertmanager-0.15.3+ds/ui/app/src/Views/SilenceView/Views.elm000066400000000000000000000076171341674552200261530ustar00rootroot00000000000000module Views.SilenceView.Views exposing (view) import Alerts.Types exposing (Alert) import Dialog import Html exposing (Html, b, button, div, h1, h2, h3, label, p, span, text) import Html.Attributes exposing (class, href) import Html.Events exposing (onClick) import Silences.Types exposing (Silence, stateToString) import Types exposing (Msg(MsgForSilenceList, MsgForSilenceView)) import Utils.Date exposing (dateTimeFormat) import Utils.List import Utils.Types exposing (ApiData(Failure, Initial, Loading, Success)) import Utils.Views exposing (error, loading) import Views.Shared.SilencePreview import Views.SilenceList.SilenceView exposing (editButton) import Views.SilenceList.Types exposing (SilenceListMsg(DestroySilence)) import Views.SilenceView.Types exposing (Model, SilenceViewMsg(ConfirmDestroySilence, Reload)) view : Model -> Html Msg view { silence, alerts, showConfirmationDialog } = case silence of Success sil -> if showConfirmationDialog then viewSilence alerts sil True else viewSilence alerts sil False Initial -> loading Loading -> loading Failure msg -> error msg viewSilence : ApiData (List Alert) -> Silence -> Bool -> Html Msg viewSilence alerts silence showPromptDialog = div [] [ h1 [] [ text "Silence" , span [ class "ml-3" ] [ editButton silence , expireButton silence False ] ] , formGroup "ID" <| text silence.id , formGroup "Starts at" <| text <| dateTimeFormat silence.startsAt , formGroup "Ends at" <| text <| dateTimeFormat silence.endsAt , formGroup "Updated at" <| text <| dateTimeFormat silence.updatedAt , formGroup "Created by" <| text silence.createdBy , formGroup "Comment" <| text silence.comment , formGroup "State" <| text <| stateToString silence.status.state , formGroup "Matchers" <| div [] <| List.map (Utils.List.mstring >> Utils.Views.labelButton Nothing) silence.matchers , formGroup "Affected alerts" <| Views.Shared.SilencePreview.view alerts , Dialog.view (if showPromptDialog then Just (confirmSilenceDeleteView silence True) else Nothing ) ] confirmSilenceDeleteView : Silence -> Bool -> Dialog.Config Msg confirmSilenceDeleteView silence refresh = { closeMessage = Just (MsgForSilenceView (Reload silence.id)) , containerClass = Nothing , header = Just (h3 [] [ text "Expire Silence" ]) , body = Just (text "Are you sure you want to expire this silence?") , footer = Just (button [ class "btn btn-success" , onClick (MsgForSilenceList (DestroySilence silence refresh)) ] [ text "Confirm" ] ) } formGroup : String -> Html Msg -> Html Msg formGroup key content = div [ class "form-group row" ] [ label [ class "col-2 col-form-label" ] [ b [] [ text key ] ] , div [ class "col-10 d-flex align-items-center" ] [ content ] ] expireButton : Silence -> Bool -> Html Msg expireButton silence refresh = case silence.status.state of Silences.Types.Expired -> text "" Silences.Types.Active -> button [ class "btn btn-outline-danger border-0" , onClick (MsgForSilenceView (ConfirmDestroySilence silence refresh)) ] [ text "Expire" ] Silences.Types.Pending -> button [ class "btn btn-outline-danger border-0" , onClick (MsgForSilenceView (ConfirmDestroySilence silence refresh)) ] [ text "Delete" ] prometheus-alertmanager-0.15.3+ds/ui/app/src/Views/Status/000077500000000000000000000000001341674552200234125ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/ui/app/src/Views/Status/Parsing.elm000066400000000000000000000002251341674552200255130ustar00rootroot00000000000000module Views.Status.Parsing exposing (statusParser) import UrlParser exposing (Parser, s) statusParser : Parser a a statusParser = s "status" prometheus-alertmanager-0.15.3+ds/ui/app/src/Views/Status/Types.elm000066400000000000000000000006301341674552200252140ustar00rootroot00000000000000module Views.Status.Types exposing (StatusMsg(..), StatusModel, initStatusModel) import Status.Types exposing (StatusResponse) import Utils.Types exposing (ApiData(Initial)) type StatusMsg = NewStatus (ApiData StatusResponse) | InitStatusView type alias StatusModel = { statusInfo : ApiData StatusResponse } initStatusModel : StatusModel initStatusModel = { statusInfo = Initial } prometheus-alertmanager-0.15.3+ds/ui/app/src/Views/Status/Updates.elm000066400000000000000000000007621341674552200255230ustar00rootroot00000000000000module Views.Status.Updates exposing (update) import Types exposing (Msg(MsgForStatus), Model) import Views.Status.Types exposing (StatusMsg(..)) import Status.Api exposing (getStatus) update : StatusMsg -> Model -> String -> ( Model, Cmd Msg ) update msg model basePath = case msg of NewStatus apiResponse -> ( { model | status = { statusInfo = apiResponse } }, Cmd.none ) InitStatusView -> ( model, getStatus basePath (NewStatus >> MsgForStatus) ) prometheus-alertmanager-0.15.3+ds/ui/app/src/Views/Status/Views.elm000066400000000000000000000105671341674552200252170ustar00rootroot00000000000000module Views.Status.Views exposing (view) import Html exposing (..) import Html.Attributes exposing (class, style, classList) import Status.Types exposing (StatusResponse, VersionInfo, ClusterStatus, ClusterPeer) import Types exposing (Msg(MsgForStatus)) import Utils.Types exposing (ApiData(Failure, Success, Loading, Initial)) import Views.Status.Types exposing (StatusModel) import Utils.Views view : StatusModel -> Html Types.Msg view { statusInfo } = case statusInfo of Success info -> viewStatusInfo info Initial -> Utils.Views.loading Loading -> Utils.Views.loading Failure msg -> Utils.Views.error msg viewStatusInfo : StatusResponse -> Html Types.Msg viewStatusInfo status = div [] [ h1 [] [ text "Status" ] , div [ class "form-group row" ] [ b [ class "col-sm-2" ] [ text "Uptime:" ] , div [ class "col-sm-10" ] [ text status.uptime ] ] , viewClusterStatus status.clusterStatus , viewVersionInformation status.versionInfo , viewConfig status.config ] viewConfig : String -> Html Types.Msg viewConfig config = div [] [ h2 [] [ text "Config" ] , pre [ class "p-4", style [ ( "background", "#f7f7f9" ), ( "font-family", "monospace" ) ] ] [ code [] [ text config ] ] ] viewClusterStatus : Maybe ClusterStatus -> Html Types.Msg viewClusterStatus clusterStatus = case clusterStatus of Just clusterStatus -> span [] [ h2 [] [ text "Cluster Status" ] , div [ class "form-group row" ] [ b [ class "col-sm-2" ] [ text "Name:" ] , div [ class "col-sm-10" ] [ text clusterStatus.name ] ] , div [ class "form-group row" ] [ b [ class "col-sm-2" ] [ text "Status:" ] , div [ class "col-sm-10" ] [ span [ classList [ ( "badge", True ) , ( "badge-success", clusterStatus.status == "ready" ) , ( "badge-warning", clusterStatus.status == "settling" ) ] ] [ text clusterStatus.status ] ] ] , div [ class "form-group row" ] [ b [ class "col-sm-2" ] [ text "Peers:" ] , ul [ class "col-sm-10" ] <| List.map viewClusterPeer clusterStatus.peers ] ] Nothing -> span [] [ h2 [] [ text "Mesh Status" ] , div [ class "form-group row" ] [ div [ class "col-sm-10" ] [ text "Mesh not configured" ] ] ] viewClusterPeer : ClusterPeer -> Html Types.Msg viewClusterPeer peer = li [] [ div [ class "" ] [ b [ class "" ] [ text "Name: " ] , text peer.name ] , div [ class "" ] [ b [ class "" ] [ text "Address: " ] , text peer.address ] ] viewVersionInformation : VersionInfo -> Html Types.Msg viewVersionInformation versionInfo = span [] [ h2 [] [ text "Version Information" ] , div [ class "form-group row" ] [ b [ class "col-sm-2" ] [ text "Branch:" ], div [ class "col-sm-10" ] [ text versionInfo.branch ] ] , div [ class "form-group row" ] [ b [ class "col-sm-2" ] [ text "BuildDate:" ], div [ class "col-sm-10" ] [ text versionInfo.buildDate ] ] , div [ class "form-group row" ] [ b [ class "col-sm-2" ] [ text "BuildUser:" ], div [ class "col-sm-10" ] [ text versionInfo.buildUser ] ] , div [ class "form-group row" ] [ b [ class "col-sm-2" ] [ text "GoVersion:" ], div [ class "col-sm-10" ] [ text versionInfo.goVersion ] ] , div [ class "form-group row" ] [ b [ class "col-sm-2" ] [ text "Revision:" ], div [ class "col-sm-10" ] [ text versionInfo.revision ] ] , div [ class "form-group row" ] [ b [ class "col-sm-2" ] [ text "Version:" ], div [ class "col-sm-10" ] [ text versionInfo.version ] ] ] prometheus-alertmanager-0.15.3+ds/ui/app/tests/000077500000000000000000000000001341674552200214055ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/ui/app/tests/.gitignore000066400000000000000000000000141341674552200233700ustar00rootroot00000000000000/elm-stuff/ prometheus-alertmanager-0.15.3+ds/ui/app/tests/Filter.elm000066400000000000000000000074271341674552200233430ustar00rootroot00000000000000module Filter exposing (..) import Test exposing (..) import Expect import Fuzz exposing (list, int, tuple, string) import Utils.Filter exposing (Matcher, MatchOperator(Eq, RegexMatch)) import Helpers exposing (isNotEmptyTrimmedAlphabetWord) parseMatcher : Test parseMatcher = describe "parseMatcher" [ test "should parse empty matcher string" <| \() -> Expect.equal Nothing (Utils.Filter.parseMatcher "") , test "should parse empty matcher value" <| \() -> Expect.equal (Just (Matcher "alertname" Eq "")) (Utils.Filter.parseMatcher "alertname=\"\"") , fuzz (tuple ( string, string )) "should parse random matcher string" <| \( key, value ) -> if List.map isNotEmptyTrimmedAlphabetWord [ key, value ] /= [ True, True ] then Expect.equal Nothing (Utils.Filter.parseMatcher <| String.join "" [ key, "=", value ]) else Expect.equal (Just (Matcher key Eq value)) (Utils.Filter.parseMatcher <| String.join "" [ key, "=", "\"", value, "\"" ]) ] generateQueryString : Test generateQueryString = describe "generateQueryString" [ test "should default silenced & inhibited parameters to false if showSilenced is Nothing" <| \() -> Expect.equal "?silenced=false&inhibited=false" (Utils.Filter.generateQueryString { receiver = Nothing, group = Nothing, text = Nothing, showSilenced = Nothing, showInhibited = Nothing }) , test "should not render keys with Nothing value except the silenced and inhibited parameters" <| \() -> Expect.equal "?silenced=false&inhibited=false" (Utils.Filter.generateQueryString { receiver = Nothing, group = Nothing, text = Nothing, showSilenced = Nothing, showInhibited = Nothing }) , test "should not render filter key with empty value" <| \() -> Expect.equal "?silenced=false&inhibited=false" (Utils.Filter.generateQueryString { receiver = Nothing, group = Nothing, text = Just "", showSilenced = Nothing, showInhibited = Nothing }) , test "should render filter key with values" <| \() -> Expect.equal "?silenced=false&inhibited=false&filter=%7Bfoo%3D%22bar%22%2C%20baz%3D~%22quux.*%22%7D" (Utils.Filter.generateQueryString { receiver = Nothing, group = Nothing, text = Just "{foo=\"bar\", baz=~\"quux.*\"}", showSilenced = Nothing, showInhibited = Nothing }) , test "should render silenced key with bool" <| \() -> Expect.equal "?silenced=true&inhibited=false" (Utils.Filter.generateQueryString { receiver = Nothing, group = Nothing, text = Nothing, showSilenced = Just True, showInhibited = Nothing }) , test "should render inhibited key with bool" <| \() -> Expect.equal "?silenced=false&inhibited=true" (Utils.Filter.generateQueryString { receiver = Nothing, group = Nothing, text = Nothing, showSilenced = Nothing, showInhibited = Just True }) ] stringifyFilter : Test stringifyFilter = describe "stringifyFilter" [ test "empty" <| \() -> Expect.equal "" (Utils.Filter.stringifyFilter []) , test "non-empty" <| \() -> Expect.equal "{foo=\"bar\", baz=~\"quux.*\"}" (Utils.Filter.stringifyFilter [ { key = "foo", op = Eq, value = "bar" } , { key = "baz", op = RegexMatch, value = "quux.*" } ] ) ] prometheus-alertmanager-0.15.3+ds/ui/app/tests/Helpers.elm000066400000000000000000000012701341674552200235060ustar00rootroot00000000000000module Helpers exposing (isNotEmptyTrimmedAlphabetWord) import String isNotEmptyTrimmedAlphabetWord : String -> Bool isNotEmptyTrimmedAlphabetWord string = let stringLength = String.length string in stringLength /= 0 && String.length (String.filter isLetter string) == stringLength isLetter : Char -> Bool isLetter char = String.contains (String.fromChar char) (lowerCaseAlphabet) || String.contains (String.fromChar char) (upperCaseAlphabet) lowerCaseAlphabet : String lowerCaseAlphabet = "abcdefghijklmnopqrstuvwxyz" upperCaseAlphabet : String upperCaseAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" prometheus-alertmanager-0.15.3+ds/ui/app/tests/Match.elm000066400000000000000000000034311341674552200231410ustar00rootroot00000000000000module Match exposing (..) import Test exposing (..) import Expect import Utils.Match exposing (jaroWinkler, consecutiveChars) testJaroWinkler : Test testJaroWinkler = describe "jaroWinkler" [ test "should find the right values 1" <| \() -> Expect.greaterThan (jaroWinkler "zi" "zone") (jaroWinkler "zo" "zone") , test "should find the right values 2" <| \() -> Expect.greaterThan (jaroWinkler "hook" "alertname") (jaroWinkler "de" "dev") , test "should find the right values 3" <| \() -> Expect.equal 0.0 (jaroWinkler "l" "zone") , test "should find the right values 4" <| \() -> Expect.equal 1.0 (jaroWinkler "zone" "zone") , test "should find the right values 5" <| \() -> Expect.greaterThan 0.688 (jaroWinkler "atleio3tefdoisahdf" "attributefdoiashfoihfeowfh9w8f9afaw9fahw") ] testConsecutiveChars : Test testConsecutiveChars = describe "consecutiveChars" [ test "should find the consecutiveChars 1" <| \() -> Expect.equal "zo" (consecutiveChars "zo" "bozo") , test "should find the consecutiveChars 2" <| \() -> Expect.equal "zo" (consecutiveChars "zol" "zone") , test "should find the consecutiveChars 3" <| \() -> Expect.equal "oon" (consecutiveChars "oon" "baboone") , test "should find the consecutiveChars 4" <| \() -> Expect.equal "dom" (consecutiveChars "dom" "random") ] prometheus-alertmanager-0.15.3+ds/ui/app/tests/StringUtils.elm000066400000000000000000000014301341674552200243710ustar00rootroot00000000000000module StringUtils exposing (testLinkify) import Utils.String exposing (linkify) import Test exposing (..) import Expect testLinkify : Test testLinkify = describe "linkify" [ test "should linkify a url in the middle" <| \() -> Expect.equal (linkify "word1 http://url word2") [ Err "word1 ", Ok "http://url", Err " word2" ] , test "should linkify a url in the beginning" <| \() -> Expect.equal (linkify "http://url word1 word2") [ Ok "http://url", Err " word1 word2" ] , test "should linkify a url in the end" <| \() -> Expect.equal (linkify "word1 word2 http://url") [ Err "word1 word2 ", Ok "http://url" ] ] prometheus-alertmanager-0.15.3+ds/ui/app/tests/elm-package.json000066400000000000000000000013121341674552200244430ustar00rootroot00000000000000{ "version": "1.0.0", "summary": "Test Suites", "repository": "https://github.com/prometheus/alertmanager.git", "license": "Apache-2.0", "source-directories": [ "../src", "." ], "exposed-modules": [], "dependencies": { "elm-lang/core": "5.0.0 <= v < 6.0.0", "elm-community/elm-test": "4.0.0 <= v < 5.0.0", "elm-lang/dom": "1.1.1 <= v < 2.0.0", "elm-lang/html": "2.0.0 <= v < 3.0.0", "elm-lang/http": "1.0.0 <= v < 2.0.0", "elm-lang/navigation": "2.0.1 <= v < 3.0.0", "elm-tools/parser": "2.0.0 <= v < 3.0.0", "evancz/url-parser": "2.0.1 <= v < 3.0.0" }, "elm-version": "0.18.0 <= v < 0.19.0" } prometheus-alertmanager-0.15.3+ds/ui/web.go000066400000000000000000000057231341674552200205760ustar00rootroot00000000000000// Copyright 2015 Prometheus Team // 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 ui import ( "bytes" "fmt" "io" "net/http" _ "net/http/pprof" // Comment this line to disable pprof endpoint. "path/filepath" "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/common/route" ) func serveAsset(w http.ResponseWriter, req *http.Request, fp string, logger log.Logger) { info, err := AssetInfo(fp) if err != nil { level.Warn(logger).Log("msg", "Could not get file", "err", err) w.WriteHeader(http.StatusNotFound) return } file, err := Asset(fp) if err != nil { if err != io.EOF { level.Warn(logger).Log("msg", "Could not get file", "file", fp, "err", err) } w.WriteHeader(http.StatusNotFound) return } w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") http.ServeContent(w, req, info.Name(), info.ModTime(), bytes.NewReader(file)) } // Register registers handlers to serve files for the web interface. func Register(r *route.Router, reloadCh chan<- chan error, logger log.Logger) { r.Get("/metrics", promhttp.Handler().ServeHTTP) r.Get("/", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { serveAsset(w, req, "ui/app/index.html", logger) })) r.Get("/script.js", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { serveAsset(w, req, "ui/app/script.js", logger) })) r.Get("/favicon.ico", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { serveAsset(w, req, "ui/app/favicon.ico", logger) })) r.Get("/lib/*filepath", http.HandlerFunc( func(w http.ResponseWriter, req *http.Request) { fp := route.Param(req.Context(), "filepath") serveAsset(w, req, filepath.Join("ui/app/lib", fp), logger) }, )) r.Post("/-/reload", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { errc := make(chan error) defer close(errc) reloadCh <- errc if err := <-errc; err != nil { http.Error(w, fmt.Sprintf("failed to reload config: %s", err), http.StatusInternalServerError) } })) r.Get("/-/healthy", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) fmt.Fprintf(w, "OK") })) r.Get("/-/ready", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) fmt.Fprintf(w, "OK") })) r.Get("/debug/*subpath", http.DefaultServeMux.ServeHTTP) r.Post("/debug/*subpath", http.DefaultServeMux.ServeHTTP) } prometheus-alertmanager-0.15.3+ds/vendor/000077500000000000000000000000001341674552200203435ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/vendor/github.com/000077500000000000000000000000001341674552200224025ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/vendor/github.com/oklog/000077500000000000000000000000001341674552200235155ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/vendor/github.com/oklog/oklog/000077500000000000000000000000001341674552200246305ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/vendor/github.com/oklog/oklog/LICENSE000066400000000000000000000261351341674552200256440ustar00rootroot00000000000000 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. prometheus-alertmanager-0.15.3+ds/vendor/github.com/oklog/oklog/pkg/000077500000000000000000000000001341674552200254115ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/vendor/github.com/oklog/oklog/pkg/group/000077500000000000000000000000001341674552200265455ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/vendor/github.com/oklog/oklog/pkg/group/group.go000066400000000000000000000035441341674552200302360ustar00rootroot00000000000000// Package group implements an actor-runner with deterministic teardown. It is // somewhat similar to package errgroup, except it does not require actor // goroutines to understand context semantics. This makes it suitable for use // in more circumstances; for example, goroutines which are handling // connections from net.Listeners, or scanning input from a closable io.Reader. package group // Group collects actors (functions) and runs them concurrently. // When one actor (function) returns, all actors are interrupted. // The zero value of a Group is useful. type Group struct { actors []actor } // Add an actor (function) to the group. Each actor must be pre-emptable by an // interrupt function. That is, if interrupt is invoked, execute should return. // Also, it must be safe to call interrupt even after execute has returned. // // The first actor (function) to return interrupts all running actors. // The error is passed to the interrupt functions, and is returned by Run. func (g *Group) Add(execute func() error, interrupt func(error)) { g.actors = append(g.actors, actor{execute, interrupt}) } // Run all actors (functions) concurrently. // When the first actor returns, all others are interrupted. // Run only returns when all actors have exited. // Run returns the error returned by the first exiting actor. func (g *Group) Run() error { if len(g.actors) == 0 { return nil } // Run each actor. errors := make(chan error, len(g.actors)) for _, a := range g.actors { go func(a actor) { errors <- a.execute() }(a) } // Wait for the first actor to stop. err := <-errors // Signal all actors to stop. for _, a := range g.actors { a.interrupt(err) } // Wait for all actors to stop. for i := 1; i < cap(errors); i++ { <-errors } // Return the original error. return err } type actor struct { execute func() error interrupt func(error) } prometheus-alertmanager-0.15.3+ds/vendor/github.com/prometheus/000077500000000000000000000000001341674552200245755ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/vendor/github.com/prometheus/prometheus/000077500000000000000000000000001341674552200267705ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/vendor/github.com/prometheus/prometheus/pkg/000077500000000000000000000000001341674552200275515ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/vendor/github.com/prometheus/prometheus/pkg/labels/000077500000000000000000000000001341674552200310135ustar00rootroot00000000000000prometheus-alertmanager-0.15.3+ds/vendor/github.com/prometheus/prometheus/pkg/labels/labels.go000066400000000000000000000132711341674552200326100ustar00rootroot00000000000000// Copyright 2017 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. package labels import ( "bytes" "encoding/json" "sort" "strconv" "strings" "github.com/cespare/xxhash" ) const sep = '\xff' // Well-known label names used by Prometheus components. const ( MetricName = "__name__" AlertName = "alertname" BucketLabel = "le" InstanceName = "instance" ) // Label is a key/value pair of strings. type Label struct { Name, Value string } // Labels is a sorted set of labels. Order has to be guaranteed upon // instantiation. type Labels []Label func (ls Labels) Len() int { return len(ls) } func (ls Labels) Swap(i, j int) { ls[i], ls[j] = ls[j], ls[i] } func (ls Labels) Less(i, j int) bool { return ls[i].Name < ls[j].Name } func (ls Labels) String() string { var b bytes.Buffer b.WriteByte('{') for i, l := range ls { if i > 0 { b.WriteByte(',') b.WriteByte(' ') } b.WriteString(l.Name) b.WriteByte('=') b.WriteString(strconv.Quote(l.Value)) } b.WriteByte('}') return b.String() } // MarshalJSON implements json.Marshaler. func (ls Labels) MarshalJSON() ([]byte, error) { return json.Marshal(ls.Map()) } // UnmarshalJSON implements json.Unmarshaler. func (ls *Labels) UnmarshalJSON(b []byte) error { var m map[string]string if err := json.Unmarshal(b, &m); err != nil { return err } *ls = FromMap(m) return nil } // Hash returns a hash value for the label set. func (ls Labels) Hash() uint64 { b := make([]byte, 0, 1024) for _, v := range ls { b = append(b, v.Name...) b = append(b, sep) b = append(b, v.Value...) b = append(b, sep) } return xxhash.Sum64(b) } // Copy returns a copy of the labels. func (ls Labels) Copy() Labels { res := make(Labels, len(ls)) copy(res, ls) return res } // Get returns the value for the label with the given name. // Returns an empty string if the label doesn't exist. func (ls Labels) Get(name string) string { for _, l := range ls { if l.Name == name { return l.Value } } return "" } // Has returns true if the label with the given name is present. func (ls Labels) Has(name string) bool { for _, l := range ls { if l.Name == name { return true } } return false } // Equal returns whether the two label sets are equal. func Equal(ls, o Labels) bool { if len(ls) != len(o) { return false } for i, l := range ls { if l.Name != o[i].Name || l.Value != o[i].Value { return false } } return true } // Map returns a string map of the labels. func (ls Labels) Map() map[string]string { m := make(map[string]string, len(ls)) for _, l := range ls { m[l.Name] = l.Value } return m } // New returns a sorted Labels from the given labels. // The caller has to guarantee that all label names are unique. func New(ls ...Label) Labels { set := make(Labels, 0, len(ls)) for _, l := range ls { set = append(set, l) } sort.Sort(set) return set } // FromMap returns new sorted Labels from the given map. func FromMap(m map[string]string) Labels { l := make([]Label, 0, len(m)) for k, v := range m { l = append(l, Label{Name: k, Value: v}) } return New(l...) } // FromStrings creates new labels from pairs of strings. func FromStrings(ss ...string) Labels { if len(ss)%2 != 0 { panic("invalid number of strings") } var res Labels for i := 0; i < len(ss); i += 2 { res = append(res, Label{Name: ss[i], Value: ss[i+1]}) } sort.Sort(res) return res } // Compare compares the two label sets. // The result will be 0 if a==b, <0 if a < b, and >0 if a > b. func Compare(a, b Labels) int { l := len(a) if len(b) < l { l = len(b) } for i := 0; i < l; i++ { if d := strings.Compare(a[i].Name, b[i].Name); d != 0 { return d } if d := strings.Compare(a[i].Value, b[i].Value); d != 0 { return d } } // If all labels so far were in common, the set with fewer labels comes first. return len(a) - len(b) } // Builder allows modifiying Labels. type Builder struct { base Labels del []string add []Label } // NewBuilder returns a new LabelsBuilder func NewBuilder(base Labels) *Builder { return &Builder{ base: base, del: make([]string, 0, 5), add: make([]Label, 0, 5), } } // Del deletes the label of the given name. func (b *Builder) Del(ns ...string) *Builder { for _, n := range ns { for i, a := range b.add { if a.Name == n { b.add = append(b.add[:i], b.add[i+1:]...) } } b.del = append(b.del, n) } return b } // Set the name/value pair as a label. func (b *Builder) Set(n, v string) *Builder { for i, a := range b.add { if a.Name == n { b.add[i].Value = v return b } } b.add = append(b.add, Label{Name: n, Value: v}) return b } // Labels returns the labels from the builder. If no modifications // were made, the original labels are returned. func (b *Builder) Labels() Labels { if len(b.del) == 0 && len(b.add) == 0 { return b.base } // In the general case, labels are removed, modified or moved // rather than added. res := make(Labels, 0, len(b.base)) Outer: for _, l := range b.base { for _, n := range b.del { if l.Name == n { continue Outer } } for _, la := range b.add { if l.Name == la.Name { continue Outer } } res = append(res, l) } res = append(res, b.add...) sort.Sort(res) return res } prometheus-alertmanager-0.15.3+ds/vendor/github.com/prometheus/prometheus/pkg/labels/matcher.go000066400000000000000000000037101341674552200327660ustar00rootroot00000000000000// Copyright 2017 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. package labels import ( "fmt" "regexp" ) // MatchType is an enum for label matching types. type MatchType int // Possible MatchTypes. const ( MatchEqual MatchType = iota MatchNotEqual MatchRegexp MatchNotRegexp ) func (m MatchType) String() string { typeToStr := map[MatchType]string{ MatchEqual: "=", MatchNotEqual: "!=", MatchRegexp: "=~", MatchNotRegexp: "!~", } if str, ok := typeToStr[m]; ok { return str } panic("unknown match type") } // Matcher models the matching of a label. type Matcher struct { Type MatchType Name string Value string re *regexp.Regexp } // NewMatcher returns a matcher object. func NewMatcher(t MatchType, n, v string) (*Matcher, error) { m := &Matcher{ Type: t, Name: n, Value: v, } if t == MatchRegexp || t == MatchNotRegexp { re, err := regexp.Compile("^(?:" + v + ")$") if err != nil { return nil, err } m.re = re } return m, nil } func (m *Matcher) String() string { return fmt.Sprintf("%s%s%q", m.Name, m.Type, m.Value) } // Matches returns whether the matcher matches the given string value. func (m *Matcher) Matches(s string) bool { switch m.Type { case MatchEqual: return s == m.Value case MatchNotEqual: return s != m.Value case MatchRegexp: return m.re.MatchString(s) case MatchNotRegexp: return !m.re.MatchString(s) } panic("labels.Matcher.Matches: invalid match type") } prometheus-alertmanager-0.15.3+ds/vendor/vendor.json000066400000000000000000000307111341674552200225350ustar00rootroot00000000000000{ "comment": "", "ignore": "test", "package": [ { "checksumSHA1": "KmjnydoAbofMieIWm+it5OWERaM=", "path": "github.com/alecthomas/template", "revision": "a0175ee3bccc567396460bf5acd36800cb10c49c", "revisionTime": "2016-04-05T07:15:01Z" }, { "checksumSHA1": "3wt0pTXXeS+S93unwhGoLIyGX/Q=", "path": "github.com/alecthomas/template/parse", "revision": "a0175ee3bccc567396460bf5acd36800cb10c49c", "revisionTime": "2016-04-05T07:15:01Z" }, { "checksumSHA1": "fCc3grA7vIxfBru7R3SqjcW+oLI=", "path": "github.com/alecthomas/units", "revision": "2efee857e7cfd4f3d0138cc3cbb1b4966962b93a", "revisionTime": "2015-10-22T06:55:26Z" }, { "checksumSHA1": "xp/2s4XclLL17DThGBI7jXZ4Crs=", "path": "github.com/armon/go-metrics", "revision": "9a4b6e10bed6220a1665955aa2b75afc91eb10b3", "revisionTime": "2017-10-02T18:27:31Z" }, { "checksumSHA1": "4QnLdmB1kG3N+KlDd1N+G9TWAGQ=", "path": "github.com/beorn7/perks/quantile", "revision": "1e01b2bdbae8edb393fcf555732304f34d192fc9", "revisionTime": "2016-11-21T14:22:35Z" }, { "checksumSHA1": "b55VMQZmwBl4iSpI9BfdesJI9vQ=", "path": "github.com/cenkalti/backoff", "revision": "1e01b2bdbae8edb393fcf555732304f34d192fc9", "revisionTime": "2016-11-21T14:22:35Z" }, { "checksumSHA1": "OFu4xJEIjiI8Suu+j/gabfp+y6Q=", "origin": "github.com/stretchr/testify/vendor/github.com/davecgh/go-spew/spew", "path": "github.com/davecgh/go-spew/spew", "revision": "346938d642f2ec3594ed81d874461961cd0faa76", "revisionTime": "2016-10-29T20:57:26Z" }, { "checksumSHA1": "KrIRJ4p3nRze4NcxfgX5+N9+D1M=", "path": "github.com/go-kit/kit/log", "revision": "e2b298466b32c7cd5579a9b9b07e968fc9d9452c", "revisionTime": "2017-10-21T13:24:59Z" }, { "checksumSHA1": "t7aTpDH0h4BZcGU0KkUr14QQG2w=", "path": "github.com/go-kit/kit/log/level", "revision": "e2b298466b32c7cd5579a9b9b07e968fc9d9452c", "revisionTime": "2017-10-21T13:24:59Z" }, { "checksumSHA1": "KxX/Drph+byPXBFIXaCZaCOAnrU=", "path": "github.com/go-logfmt/logfmt", "revision": "390ab7935ee28ec6b286364bba9b4dd6410cb3d5", "revisionTime": "2016-11-15T14:25:13Z" }, { "checksumSHA1": "j6vhe49MX+dyHR9rU91P6vMx55o=", "path": "github.com/go-stack/stack", "revision": "817915b46b97fd7bb80e8ab6b69f01a53ac3eebf", "revisionTime": "2017-07-24T01:23:01Z" }, { "checksumSHA1": "FhLvgtYfuKY0ow9wtLJRoeg7d6w=", "path": "github.com/gogo/protobuf/gogoproto", "revision": "616a82ed12d78d24d4839363e8f3c5d3f20627cf", "revisionTime": "2017-11-09T18:15:19Z" }, { "checksumSHA1": "6ZxSmrIx3Jd15aou16oG0HPylP4=", "path": "github.com/gogo/protobuf/proto", "revision": "c0656edd0d9eab7c66d1eb0c568f9039345796f7", "revisionTime": "2017-03-30T07:10:51Z" }, { "checksumSHA1": "F+PKpdY6PyIrxQ8b20TzsM+1JuI=", "path": "github.com/gogo/protobuf/protoc-gen-gogo/descriptor", "revision": "616a82ed12d78d24d4839363e8f3c5d3f20627cf", "revisionTime": "2017-11-09T18:15:19Z" }, { "checksumSHA1": "HPVQZu059/Rfw2bAWM538bVTcUc=", "path": "github.com/gogo/protobuf/sortkeys", "revision": "c0656edd0d9eab7c66d1eb0c568f9039345796f7", "revisionTime": "2017-03-30T07:10:51Z" }, { "checksumSHA1": "b1p91yJ1mx+abXDUtM5UV3xEiaw=", "path": "github.com/gogo/protobuf/types", "revision": "c0656edd0d9eab7c66d1eb0c568f9039345796f7", "revisionTime": "2017-03-30T07:10:51Z" }, { "checksumSHA1": "M/1HiQFl2DyE+q3pFf8swvAQ3MY=", "path": "github.com/golang/protobuf/proto", "revision": "1e01b2bdbae8edb393fcf555732304f34d192fc9", "revisionTime": "2016-11-21T14:22:35Z" }, { "checksumSHA1": "cdOCt0Yb+hdErz8NAQqayxPmRsY=", "origin": "github.com/hashicorp/go-multierror/vendor/github.com/hashicorp/errwrap", "path": "github.com/hashicorp/errwrap", "revision": "83588e72410abfbe4df460eeb6f30841ae47d4c4", "revisionTime": "2017-06-22T06:09:55Z" }, { "checksumSHA1": "Cas2nprG6pWzf05A2F/OlnjUu2Y=", "path": "github.com/hashicorp/go-immutable-radix", "revision": "8aac2701530899b64bdea735a1de8da899815220", "revisionTime": "2017-07-25T22:12:15Z" }, { "checksumSHA1": "TNlVzNR1OaajcNi3CbQ3bGbaLGU=", "path": "github.com/hashicorp/go-msgpack/codec", "revision": "fa3f63826f7c23912c15263591e65d54d080b458", "revisionTime": "2015-05-18T23:42:57Z" }, { "checksumSHA1": "g7uHECbzuaWwdxvwoyxBwgeERPk=", "path": "github.com/hashicorp/go-multierror", "revision": "83588e72410abfbe4df460eeb6f30841ae47d4c4", "revisionTime": "2017-06-22T06:09:55Z" }, { "checksumSHA1": "eCWvhgknHMj5K19ePPjIA3l401Q=", "path": "github.com/hashicorp/go-sockaddr", "revision": "9b4c5fa5b10a683339a270d664474b9f4aee62fc", "revisionTime": "2017-10-30T10:43:12Z" }, { "checksumSHA1": "9hffs0bAIU6CquiRhKQdzjHnKt0=", "path": "github.com/hashicorp/golang-lru/simplelru", "revision": "0a025b7e63adc15a622f29b0b2c4c3848243bbf6", "revisionTime": "2016-08-13T22:13:03Z" }, { "checksumSHA1": "vwj2yOi577Mmn+IfJwV8YXYeALk=", "path": "github.com/hashicorp/memberlist", "revision": "687988a0b5daaf7ed5051e5e374aef27f8254822", "revisionTime": "2017-09-19T17:31:51Z" }, { "checksumSHA1": "gKyBj05YkfuLFruAyPZ4KV9nFp8=", "path": "github.com/julienschmidt/httprouter", "revision": "975b5c4c7c21c0e3d2764200bf2aa8e34657ae6e", "revisionTime": "2017-04-30T22:20:11Z" }, { "checksumSHA1": "abKzFXAn0KDr5U+JON1ZgJ2lUtU=", "path": "github.com/kr/logfmt", "revision": "b84e30acd515aadc4b783ad4ff83aff3299bdfe0", "revisionTime": "2014-02-26T03:06:59Z" }, { "checksumSHA1": "0utJAePiKZZzPG3SxPA+xWQ3JAw=", "path": "github.com/kylelemons/godebug/diff", "revision": "1e01b2bdbae8edb393fcf555732304f34d192fc9", "revisionTime": "2016-11-21T14:22:35Z" }, { "checksumSHA1": "f1Cya7f83KJIkWcYWxT3ZZxDeZc=", "path": "github.com/kylelemons/godebug/pretty", "revision": "1e01b2bdbae8edb393fcf555732304f34d192fc9", "revisionTime": "2016-11-21T14:22:35Z" }, { "checksumSHA1": "Q2vw4HZBbnU8BLFt8VrzStwqSJg=", "path": "github.com/matttproud/golang_protobuf_extensions/pbutil", "revision": "1e01b2bdbae8edb393fcf555732304f34d192fc9", "revisionTime": "2016-11-21T14:22:35Z" }, { "checksumSHA1": "DpaOH7hx9TRqhDnhOjaXfjNsitQ=", "path": "github.com/miekg/dns", "revision": "388f6eea2949b6d9071d25f08cf5b6686da15265", "revisionTime": "2017-11-08T10:01:19Z" }, { "checksumSHA1": "AYjG78HsyaohvBTuzCgdxU48dFE=", "path": "github.com/miekg/dns/internal/socket", "revision": "388f6eea2949b6d9071d25f08cf5b6686da15265", "revisionTime": "2017-11-08T10:01:19Z" }, { "checksumSHA1": "ZYfqG6bNE3cRlbsvpJBL0bF6DSc=", "path": "github.com/mwitkow/go-conntrack", "revision": "cc309e4a22231782e8893f3c35ced0967807a33e", "revisionTime": "2016-11-29T09:58:57Z" }, { "checksumSHA1": "gkyBg/2hcIWR/8qGEeGVoHwOyfo=", "path": "github.com/oklog/oklog/pkg/group", "revision": "f857583a70c345341d679b3f27aa542c8db70a21", "revisionTime": "2017-09-18T07:00:58Z" }, { "checksumSHA1": "B1iGaUz7NrjEmCjVdIgH5pvkTe8=", "path": "github.com/oklog/ulid", "revision": "66bb6560562feca7045b23db1ae85b01260f87c5", "revisionTime": "2017-01-17T20:06:51Z" }, { "checksumSHA1": "ynJSWoF6v+3zMnh9R0QmmG6iGV8=", "path": "github.com/pkg/errors", "revision": "ff09b135c25aae272398c51a07235b90a75aa4f0", "revisionTime": "2017-03-16T20:15:38Z" }, { "checksumSHA1": "zKKp5SZ3d3ycKe4EKMNT0BqAWBw=", "origin": "github.com/stretchr/testify/vendor/github.com/pmezard/go-difflib/difflib", "path": "github.com/pmezard/go-difflib/difflib", "revision": "18a02ba4a312f95da08ff4cfc0055750ce50ae9e", "revisionTime": "2016-11-17T07:43:51Z" }, { "checksumSHA1": "4VppKBbzCSmoFfQxGNUm0TYiFCA=", "path": "github.com/prometheus/client_golang/api", "revision": "c3324c1198cf3374996e9d3098edd46a6b55afc9", "revisionTime": "2018-02-23T14:47:18Z" }, { "checksumSHA1": "I87tkF1e/hrl4d/XIKFfkPRq1ww=", "path": "github.com/prometheus/client_golang/prometheus", "revision": "d49167c4b9f3c4451707560c5c71471ff5291aaa", "revisionTime": "2018-03-19T13:17:21Z" }, { "checksumSHA1": "mIWVz1E1QJ6yZnf7ELNwLboyK4w=", "path": "github.com/prometheus/client_golang/prometheus/promhttp", "revision": "d49167c4b9f3c4451707560c5c71471ff5291aaa", "revisionTime": "2018-03-19T13:17:21Z" }, { "checksumSHA1": "DvwvOlPNAgRntBzt3b3OSRMS2N4=", "path": "github.com/prometheus/client_model/go", "revision": "1e01b2bdbae8edb393fcf555732304f34d192fc9", "revisionTime": "2016-11-21T14:22:35Z" }, { "checksumSHA1": "cxLURB7wEFsMAZ6fi5Ptfdrwdc8=", "path": "github.com/prometheus/common/config", "revision": "7600349dcfe1abd18d72d3a1770870d9800a7801", "revisionTime": "2018-05-18T15:47:59Z" }, { "checksumSHA1": "vPdC/DzEm7YbzRir2wwnpLPfay8=", "path": "github.com/prometheus/common/expfmt", "revision": "7600349dcfe1abd18d72d3a1770870d9800a7801", "revisionTime": "2018-05-18T15:47:59Z" }, { "checksumSHA1": "GWlM3d2vPYyNATtTFgftS10/A9w=", "path": "github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg", "revision": "7600349dcfe1abd18d72d3a1770870d9800a7801", "revisionTime": "2018-05-18T15:47:59Z" }, { "checksumSHA1": "EXTRY7DL9gFW8c341Dk6LDXCBn8=", "path": "github.com/prometheus/common/model", "revision": "7600349dcfe1abd18d72d3a1770870d9800a7801", "revisionTime": "2018-05-18T15:47:59Z" }, { "checksumSHA1": "Yseprf8kAFr/s7wztkQnrFuFN+8=", "path": "github.com/prometheus/common/promlog", "revision": "7600349dcfe1abd18d72d3a1770870d9800a7801", "revisionTime": "2018-05-18T15:47:59Z" }, { "checksumSHA1": "9doPk0x0LONG/idxK61JnZYcxBs=", "path": "github.com/prometheus/common/route", "revision": "7600349dcfe1abd18d72d3a1770870d9800a7801", "revisionTime": "2018-05-18T15:47:59Z" }, { "checksumSHA1": "91KYK0SpvkaMJJA2+BcxbVnyRO0=", "path": "github.com/prometheus/common/version", "revision": "7600349dcfe1abd18d72d3a1770870d9800a7801", "revisionTime": "2018-05-18T15:47:59Z" }, { "checksumSHA1": "gc+7liWFv9C7S5kS2l7qLajiWdc=", "path": "github.com/prometheus/procfs", "revision": "1e01b2bdbae8edb393fcf555732304f34d192fc9", "revisionTime": "2016-11-21T14:22:35Z" }, { "checksumSHA1": "VvJ7kr3WkRUwMKxzbnxroYVZMmg=", "path": "github.com/prometheus/prometheus/pkg/labels", "revision": "58e2a31db8de12da60783d91d748008daed95159", "revisionTime": "2018-03-15T08:59:19Z" }, { "checksumSHA1": "cKNzpMpci3WUAzXpe+tVnBv2cjE=", "path": "github.com/satori/go.uuid", "revision": "1e01b2bdbae8edb393fcf555732304f34d192fc9", "revisionTime": "2016-11-21T14:22:35Z" }, { "checksumSHA1": "tnMZLo/kR9Kqx6GtmWwowtTLlA8=", "path": "github.com/sean-/seed", "revision": "e2103e2c35297fb7e17febb81e49b312087a2372", "revisionTime": "2017-03-13T16:33:22Z" }, { "checksumSHA1": "iydUphwYqZRq3WhstEdGsbvBAKs=", "path": "github.com/stretchr/testify/assert", "revision": "1e01b2bdbae8edb393fcf555732304f34d192fc9", "revisionTime": "2016-11-21T14:22:35Z" }, { "checksumSHA1": "P9FJpir2c4G5PA46qEkaWy3l60U=", "path": "github.com/stretchr/testify/require", "revision": "1e01b2bdbae8edb393fcf555732304f34d192fc9", "revisionTime": "2016-11-21T14:22:35Z" }, { "checksumSHA1": "hKZGpbFuCrkE4s1vxubKK1aFlfI=", "path": "golang.org/x/net/context", "revision": "1e01b2bdbae8edb393fcf555732304f34d192fc9", "revisionTime": "2016-11-21T14:22:35Z" }, { "checksumSHA1": "GNnwCoCCsZTbfmQsw7A7eIf++Ms=", "path": "golang.org/x/net/context/ctxhttp", "revision": "1e01b2bdbae8edb393fcf555732304f34d192fc9", "revisionTime": "2016-11-21T14:22:35Z" }, { "checksumSHA1": "UxahDzW2v4mf/+aFxruuupaoIwo=", "path": "golang.org/x/net/internal/timeseries", "revision": "db08ff08e8622530d9ed3a0e8ac279f6d4c02196", "revisionTime": "2018-06-11T16:35:41Z" }, { "checksumSHA1": "rJn3m/27kO+2IU6KCCZ74Miby+8=", "path": "golang.org/x/net/trace", "revision": "db08ff08e8622530d9ed3a0e8ac279f6d4c02196", "revisionTime": "2018-06-11T16:35:41Z" }, { "checksumSHA1": "sToCp8GThnMnsBzsHv+L/tBYQrQ=", "path": "gopkg.in/alecthomas/kingpin.v2", "revision": "947dcec5ba9c011838740e680966fd7087a71d0d", "revisionTime": "2017-12-17T18:08:21Z" }, { "checksumSHA1": "ZSWoOPUNRr5+3dhkLK3C4cZAQPk=", "path": "gopkg.in/yaml.v2", "revision": "5420a8b6744d3b0345ab293f6fcba19c978f1183", "revisionTime": "2018-03-28T19:50:20Z", "version": "v2.2", "versionExact": "v2.2.1" } ], "rootPath": "github.com/prometheus/alertmanager" }