├── .circleci └── config.yml ├── .dockerignore ├── .github ├── CODEOWNERS └── ISSUE_TEMPLATE ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile.alertmanager ├── Dockerfile.prometheus ├── LICENSE ├── README.md ├── THIRD_PARTY_CODE_DISCLAIMER.md ├── alertmanager ├── client │ ├── client.go │ ├── client_test.go │ ├── mocks │ │ ├── AlertmanagerClient.go │ │ └── TemplateClient.go │ ├── template_client.go │ ├── template_client_test.go │ └── testdata │ │ └── test.tmpl ├── common │ └── config.go ├── config │ ├── config.go │ ├── config_test.go │ ├── receiver.go │ ├── receiver_test.go │ └── route.go ├── docs │ ├── swagger-v1.yml │ └── swagger.yaml ├── handlers │ ├── handlers.go │ ├── handlers_test.go │ ├── template_handlers.go │ └── template_handlers_test.go ├── migration │ └── migration.go ├── server.go └── testcommon │ └── configs.go ├── default_configs ├── alertmanager.yml └── prometheus.yml ├── docker-compose.yml ├── fsclient ├── fsclient.go └── mocks │ └── FSClient.go ├── go.mod ├── go.sum ├── helm └── prometheus-configmanager │ ├── Chart.yaml │ ├── templates │ ├── alertmanager-configurer.deployment.yaml │ ├── alertmanager-configurer.service.yaml │ ├── alerts-ui.deployment.yaml │ ├── alerts-ui.service.yaml │ ├── prometheus-configurer.deployment.yaml │ └── prometheus-configurer.service.yaml │ └── values.yaml ├── prometheus ├── alert │ ├── alert_rule.go │ ├── alert_rule_test.go │ ├── client.go │ ├── client_test.go │ ├── file_locker.go │ ├── file_locker_test.go │ └── mocks │ │ ├── DirectoryClient.go │ │ └── PrometheusAlertClient.go ├── docs │ ├── swagger-v1.yml │ └── swagger.yaml ├── handlers │ ├── handlers.go │ └── handlers_test.go └── server.go ├── restrictor ├── query_restrictor.go └── query_restrictor_test.go └── ui ├── .dockerignore ├── .flowconfig ├── .gitignore ├── Dockerfile ├── package.json ├── public ├── index.html └── manifest.json ├── src ├── APIUtil.js ├── App.css ├── App.js ├── TenantSelector.js └── index.js └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # This source code is licensed under the MIT license found in the 3 | # LICENSE file in the root directory of this source tree. 4 | 5 | version: 2.1 6 | 7 | aliases: 8 | - &mktestdir 9 | run: 10 | name: Create results directory 11 | command: mkdir -p ~/test-results 12 | 13 | - &storetestdir 14 | store_test_results: 15 | path: ~/test-results 16 | 17 | executors: 18 | golangci: 19 | parameters: 20 | tag: 21 | default: v1.23.8 22 | type: string 23 | docker: 24 | - image: golangci/golangci-lint:<> 25 | 26 | orbs: 27 | docker: circleci/docker@1.0.0 28 | go: circleci/go@1.1.0 29 | helm: circleci/helm@1.2.0 30 | 31 | commands: 32 | 33 | getmods: 34 | steps: 35 | - go/load-cache 36 | - go/mod-download 37 | - go/save-cache 38 | 39 | helm-lint: 40 | parameters: 41 | chart: 42 | type: string 43 | working_directory: 44 | type: string 45 | steps: 46 | - run: 47 | name: Lint <> chart 48 | working_directory: <> 49 | command: helm lint --strict <> 50 | 51 | jobs: 52 | build: 53 | docker: 54 | - image: circleci/golang:1.13 55 | steps: 56 | - checkout 57 | - *mktestdir 58 | - run: 59 | name: Run unit tests 60 | command: gotestsum -f short-verbose --junitfile ~/test-results/unit.xml 61 | - setup_remote_docker: 62 | docker_layer_caching: true 63 | - run: docker build -t prometheus-configurer -f Dockerfile.prometheus . 64 | - run: docker build -t alertmanager-configurer -f Dockerfile.alertmanager . 65 | - run: 66 | name: "Start Prometheus Service and Check That it’s Running" 67 | command: | 68 | docker run -d --name prometheus-configurer prometheus-configurer 69 | docker exec prometheus-configurer apk add curl 70 | docker exec prometheus-configurer curl --retry 10 --retry-connrefused http://localhost:9100 71 | - run: 72 | name: "Start Alertmanager Service and Check That it’s Running" 73 | command: | 74 | docker run -d --name alertmanager-configurer alertmanager-configurer 75 | docker exec alertmanager-configurer apk add curl 76 | docker exec alertmanager-configurer curl --retry 10 --retry-connrefused http://localhost:9101 77 | 78 | - store_artifacts: 79 | path: /tmp/test-results 80 | destination: raw-test-output 81 | - *storetestdir 82 | 83 | lint: 84 | executor: golangci 85 | steps: 86 | - checkout 87 | - *mktestdir 88 | - getmods 89 | - run: 90 | name: Lint tests 91 | command: golangci-lint run --out-format junit-xml > ~/test-results/lint.xml 92 | - *storetestdir 93 | 94 | helm-lint: 95 | executor: docker/docker 96 | parameters: 97 | working_directory: 98 | default: helm 99 | type: string 100 | steps: 101 | - checkout 102 | - helm/install-helm-client 103 | - helm-lint: 104 | chart: prometheus-configmanager 105 | working_directory: <> 106 | 107 | copyright-lint: 108 | executor: golangci 109 | steps: 110 | - checkout 111 | - getmods 112 | - run: 113 | name: Copyright Check 114 | command: go run github.com/google/addlicense --check -c Facebook -y 2004-present -l mit ./ 115 | 116 | workflows: 117 | version: 2.1 118 | all: 119 | jobs: 120 | - lint 121 | - helm-lint 122 | - copyright-lint 123 | - build 124 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | /ui 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @scott8440 @rckclmbr @murtadha @aclave1 @karthiksubraveti 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report a bug 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Your Environment 11 | 12 | - **Version:** 13 | - **Deployment Environment:** e.g. kubernetes, local docker-compose 14 | 15 | ### Describe the Issue 16 | 17 | A clear and concise description of what the bug is. 18 | 19 | **To Reproduce** 20 | 1. Go to '...' 21 | 2. Click on '....' 22 | 3. Scroll down to '....' 23 | 4. See error 24 | 25 | **Expected behavior** 26 | A clear and concise description of what you expected to happen. 27 | 28 | **Screenshots** 29 | If applicable, add screenshots to help explain your problem. 30 | 31 | **Additional context** 32 | Add any other context about the problem here (e.g. logs). 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn-error.log 3 | **/.env 4 | coverage 5 | .idea/ 6 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to make participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies within all project spaces, and it also applies when 49 | an individual is representing the project or its community in public spaces. 50 | Examples of representing a project or community include using an official 51 | project e-mail address, posting via an official social media account, or acting 52 | as an appointed representative at an online or offline event. Representation of 53 | a project may be further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at . All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Prometheus-Configmanager 2 | We want to make contributing to this project as easy and transparent as 3 | possible. 4 | 5 | ## Pull Requests 6 | We actively welcome your pull requests. 7 | 8 | 1. Fork the repo and create your branch from `master`. 9 | 2. If you've added code that should be tested, add tests. 10 | 3. If you've changed APIs, update the documentation. 11 | 4. Ensure the test suite passes. 12 | 5. Make sure your code lints. 13 | 6. If you haven't already, complete the Contributor License Agreement ("CLA"). 14 | 15 | ## Contributor License Agreement ("CLA") 16 | In order to accept your pull request, we need you to submit a CLA. You only need 17 | to do this once to work on any of Facebook's open source projects. 18 | 19 | Complete your CLA here: 20 | 21 | ## Issues 22 | We use GitHub issues to track public bugs. Please ensure your description is 23 | clear and has sufficient instructions to be able to reproduce the issue. 24 | 25 | Facebook has a [bounty program](https://www.facebook.com/whitehat/) for the safe 26 | disclosure of security bugs. In those cases, please go through the process 27 | outlined on that page and do not file a public issue. 28 | 29 | ## License 30 | By contributing to Prometheus-Configmanager, you agree that your contributions will be licensed 31 | under the LICENSE file in the root directory of this source tree. 32 | -------------------------------------------------------------------------------- /Dockerfile.alertmanager: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # 3 | # This source code is licensed under the MIT license found in the 4 | # LICENSE file in the root directory of this source tree. 5 | 6 | # Golang image to build prom_alertconfig service 7 | FROM golang:1.13-alpine3.11 as go 8 | 9 | RUN apk add git 10 | 11 | WORKDIR /app/alertmanager-configurer 12 | 13 | COPY go.mod . 14 | COPY go.sum . 15 | 16 | RUN go mod download 17 | 18 | COPY . . 19 | 20 | # Build alertmanager service 21 | WORKDIR alertmanager 22 | RUN go build -i -o /build/bin/alertmanager_configurer 23 | 24 | # Build migration CLI 25 | RUN go build -i -o /build/bin/migration 26 | 27 | FROM alpine:3.11 28 | 29 | COPY --from=go /build/bin/alertmanager_configurer /bin/alertmanager_configurer 30 | COPY --from=go /build/bin/migration /bin/migration 31 | 32 | # Copy config files 33 | COPY default_configs /etc/configs 34 | 35 | ENTRYPOINT ["alertmanager_configurer"] 36 | -------------------------------------------------------------------------------- /Dockerfile.prometheus: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # 3 | # This source code is licensed under the MIT license found in the 4 | # LICENSE file in the root directory of this source tree. 5 | 6 | FROM golang:1.13-alpine3.11 as go 7 | 8 | RUN apk add git 9 | 10 | WORKDIR /app/prometheus-configurer 11 | 12 | COPY go.mod . 13 | COPY go.sum . 14 | 15 | RUN go mod download 16 | 17 | COPY . . 18 | 19 | # Build prometheus_configurer service 20 | WORKDIR prometheus 21 | RUN go build -i -o /build/bin/prometheus_configurer 22 | 23 | FROM alpine:3.11 24 | 25 | COPY --from=go /build/bin/prometheus_configurer /bin/prometheus_configurer 26 | 27 | # Copy config files 28 | COPY default_configs /etc/configs 29 | 30 | ENTRYPOINT ["prometheus_configurer"] 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Facebook, Inc. and its affiliates. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Prometheus-Configmanager 2 | 3 | Prometheus Configmanager consists of two HTTP-based configuration services for Prometheus and Alertmanager configurations. Both Prometheus and Alertmanager use yaml files configuration, and are only modifiable by directly rewriting these files and then sending a request to the respective service to reload the configuration files. Configmanager provides an HTTP API to modify and reload these configuration files remotely (alertmanager.yml and alert rules files used by prometheus). 4 | 5 | ## Multi-tenancy 6 | 7 | Both configmanagers are built with multiple tenants in mind. API paths require a `tenant_id` which uniquely identifies a tenant using the system. While multiple tenants operate on the same configuration, there is no worry about conflict as every alerting rule, routing receiver, or other component is kept distinct by using the tenant ID. 8 | 9 | The basic way of providing multitenancy in prometheus components is by using labels. For example, in a multitenant alertmanager-configurer setup, each alert is first routed on the tenancy label, and then the routing tree is distinct for each tenant. With prometheus, alerting rules can be restricted so that each rule can only be triggered by metrics which have the label `{tenancyLabel: tenant_id}`. 10 | 11 | ### Prometheus 12 | 13 | Command line Arguments: 14 | ``` 15 | -port string 16 | Port to listen for requests. Default is 9100 (default "9100") 17 | -prometheusURL string 18 | URL of the prometheus instance that is reading these rules. Default is prometheus:9090 (default "prometheus:9090") 19 | -multitenant-label string 20 | The label name to segment alerting rules to enable multi-tenant support, having each tenant's alerts in a separate file. Default is tenant (default "tenant") 21 | -restrict-queries 22 | If this flag is set all alert rule expressions will be restricted to only match series with {=} 23 | -rules-dir string 24 | Directory to write rules files. Default is '.' (default ".") 25 | ``` 26 | 27 | ### Alertmanager 28 | 29 | Command line Arguments 30 | ``` 31 | -alertmanager-conf string 32 | Path to alertmanager configuration file. Default is ./alertmanager.yml (default "./alertmanager.yml") 33 | -alertmanagerURL string 34 | URL of the alertmanager instance that is being used. Default is alertmanager:9093 (default "alertmanager:9093") 35 | -multitenant-label string 36 | LabelName to use for enabling multitenancy through route matching. Leave empty for single tenant use cases. 37 | -port string 38 | Port to listen for requests. Default is 9101 (default "9101") 39 | ``` 40 | 41 | 42 | ## HTTP API Documentation 43 | 44 | Swagger documentation for the APIs can be found at `prometheus/docs/swagger-v1.yml` and `alertmanager/docs/swagger-v1.yml` 45 | 46 | ## Operation 47 | 48 | The general way of using these services is by letting them take control of your Prometheus and Alertmanager configuration files. As such, they should be run on the same pod (if using kubernetes) as those services. Once set up, it is best to not edit these files manually as you may put it in a bad state that configmanager is not able to understand. Note that prometheus.yml is not directly modified by these services, so that is safe so long as you have a section like below: 49 | 50 | ``` 51 | rule_files: 52 | - '/etc/prometheus/alert_rules/*_rules.yml' 53 | ``` 54 | 55 | Where at least one of the elements in the array is pointed to the same directory that configmanager is writing the rules files (controlled by command line arguments). 56 | 57 | 58 | ## Building Docker Containers 59 | 60 | Use the following commands to build the containers: 61 | ``` 62 | docker build -t prometheus-configurer -f Dockerfile.prometheus . 63 | docker build -t alertmanager-configurer -f Dockerfile.alertmanager . 64 | ``` 65 | 66 | ## Deploying on Minikube 67 | 68 | On your local machine start Minikube. Your exact command may be different due to different VM providers. 69 | ``` 70 | minikube start --mount --mount-string "/prometheus-configmanager:/prometheus-configmanager" 71 | ``` 72 | 73 | SSH into the minikube vm with 74 | ``` 75 | minikube ssh 76 | ``` 77 | From minikube build all the docker containers: 78 | ``` 79 | $ cd /prometheus-configmanager 80 | $ docker build -f Dockerfile.alertmanager -t alertmanager-configurer . 81 | $ docker build -f Dockerfile.promtheus -t prom-configurer . 82 | $ cd ui 83 | $ docker build -t alerts-ui . 84 | ``` 85 | Back on your host machine: 86 | ``` 87 | $ cd 88 | $ helm init 89 | $ helm install --name prometheus-configmanager . 90 | ``` 91 | You can then check the status of the deployment with 92 | ``` 93 | $ helm status prometheus-configmanager 94 | ``` 95 | And you should see something like this: 96 | ``` 97 | LAST DEPLOYED: Mon Jun 29 14:02:43 2020 98 | NAMESPACE: default 99 | STATUS: DEPLOYED 100 | 101 | RESOURCES: 102 | ==> v1/Deployment 103 | NAME AGE 104 | alertmanager-configurer 34m 105 | alerts-ui 34m 106 | prometheus-configurer 34m 107 | 108 | ==> v1/Pod(related) 109 | NAME AGE 110 | alertmanager-configurer-5b57b9b5d5-hns7d 34m 111 | alerts-ui-85566df78c-7wcl2 34m 112 | prometheus-configurer-575567dd95-x4mnj 34m 113 | 114 | ==> v1/Service 115 | NAME AGE 116 | alertmanager-configurer 34m 117 | alerts-ui 34m 118 | prometheus-configurer 34m 119 | ``` 120 | 121 | ## Third-Party Code Disclaimer 122 | Prometheus-Configmanager contains dependencies which are not maintained by the maintainers of this project. Please read the disclaimer at THIRD_PARTY_CODE_DISCLAIMER.md. 123 | 124 | ## License 125 | 126 | Prometheus-Configmanager is MIT License licensed, as found in the LICENSE file. 127 | -------------------------------------------------------------------------------- /THIRD_PARTY_CODE_DISCLAIMER.md: -------------------------------------------------------------------------------- 1 | DISCLAIMER OF THIRD-PARTY CODE 2 | 3 | This software contains dependencies and third party software which are not maintained by the maintainers of this project. A link to the licenses of each dependency is provided in the LICENSE file, and in general the Disclaimer of Warranty of the MIT License (copied below) is applicable to all code provided in this project, as well as any dependencies required for building this project. 4 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 5 | 6 | 7 | List of software dependencies and associated licenses: 8 | 9 | Alpine Linux: https://github.com/alpinelinux/docker-alpine/blob/master/LICENSE 10 | 11 | Glog:https://github.com/golang/glog/blob/master/LICENSE 12 | 13 | Echo: https://github.com/labstack/echo/blob/master/LICENSE 14 | 15 | Prometheus: https://github.com/prometheus/prometheus/blob/master/LICENSE 16 | 17 | Prometheus Alertmanager: https://github.com/prometheus/alertmanager/blob/master/LICENSE 18 | 19 | Testify: https://github.com/stretchr/testify/blob/master/LICENSE 20 | -------------------------------------------------------------------------------- /alertmanager/client/client_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package client 9 | 10 | import ( 11 | "regexp" 12 | "testing" 13 | 14 | "gopkg.in/yaml.v2" 15 | 16 | "github.com/facebookincubator/prometheus-configmanager/alertmanager/config" 17 | tc "github.com/facebookincubator/prometheus-configmanager/alertmanager/testcommon" 18 | "github.com/facebookincubator/prometheus-configmanager/fsclient/mocks" 19 | "github.com/facebookincubator/prometheus-configmanager/prometheus/alert" 20 | 21 | "github.com/stretchr/testify/assert" 22 | "github.com/stretchr/testify/mock" 23 | ) 24 | 25 | const ( 26 | testNID = "test" 27 | otherNID = "other" 28 | testAlertmanagerFile = `global: 29 | resolve_timeout: 5m 30 | http_config: {} 31 | smtp_hello: localhost 32 | smtp_require_tls: true 33 | pagerduty_url: https://events.pagerduty.com/v2/enqueue 34 | hipchat_api_url: https://api.hipchat.com/ 35 | opsgenie_api_url: https://api.opsgenie.com/ 36 | wechat_api_url: https://qyapi.weixin.qq.com/cgi-bin/ 37 | victorops_api_url: https://alert.victorops.com/integrations/generic/20131114/alert/ 38 | route: 39 | receiver: null_receiver 40 | group_by: 41 | - alertname 42 | group_wait: 10s 43 | group_interval: 10s 44 | repeat_interval: 1h 45 | routes: 46 | - receiver: other_tenant_base_route 47 | match: 48 | tenantID: other 49 | receivers: 50 | - name: null_receiver 51 | - name: test_receiver 52 | - name: receiver 53 | - name: other_tenant_base_route 54 | - name: sample_tenant_base_route 55 | - name: test_slack 56 | slack_configs: 57 | - api_url: http://slack.com/12345 58 | channel: string 59 | username: string 60 | - name: other_receiver 61 | slack_configs: 62 | - api_url: http://slack.com/54321 63 | channel: string 64 | username: string 65 | - name: test_webhook 66 | webhook_configs: 67 | - url: http://webhook.com/12345 68 | send_resolved: true 69 | - name: test_email 70 | email_configs: 71 | - to: test@mail.com 72 | from: testUser 73 | smarthost: http://mail-server.com 74 | headers: 75 | name: value 76 | foo: bar 77 | templates: 78 | - "path/to/file1" 79 | - "path/to/file2" 80 | - "path/to/file3" 81 | ` 82 | ) 83 | 84 | func TestClient_CreateReceiver(t *testing.T) { 85 | client, fsClient, _ := newTestClient() 86 | // Create Slack Receiver 87 | err := client.CreateReceiver(testNID, tc.SampleSlackReceiver) 88 | assert.NoError(t, err) 89 | fsClient.AssertCalled(t, "WriteFile", "test/alertmanager.yml", mock.Anything, mock.Anything) 90 | 91 | // Create Webhook Receiver 92 | err = client.CreateReceiver(testNID, tc.SampleWebhookReceiver) 93 | assert.NoError(t, err) 94 | fsClient.AssertCalled(t, "WriteFile", "test/alertmanager.yml", mock.Anything, mock.Anything) 95 | 96 | // Create Email receiver 97 | err = client.CreateReceiver(testNID, tc.SampleEmailReceiver) 98 | assert.NoError(t, err) 99 | fsClient.AssertCalled(t, "WriteFile", "test/alertmanager.yml", mock.Anything, mock.Anything) 100 | 101 | // create duplicate receiver 102 | err = client.CreateReceiver(testNID, config.Receiver{Name: "receiver"}) 103 | assert.Regexp(t, regexp.MustCompile("notification config name \".*receiver\" is not unique"), err.Error()) 104 | } 105 | 106 | func TestClient_GetReceivers(t *testing.T) { 107 | client, _, _ := newTestClient() 108 | recs, err := client.GetReceivers(testNID) 109 | assert.NoError(t, err) 110 | assert.Equal(t, 4, len(recs)) 111 | assert.Equal(t, "receiver", recs[0].Name) 112 | assert.Equal(t, "slack", recs[1].Name) 113 | assert.Equal(t, "webhook", recs[2].Name) 114 | assert.Equal(t, "email", recs[3].Name) 115 | 116 | recs, err = client.GetReceivers(otherNID) 117 | assert.NoError(t, err) 118 | assert.Equal(t, 1, len(recs)) 119 | 120 | recs, err = client.GetReceivers("bad_nid") 121 | assert.NoError(t, err) 122 | assert.Equal(t, 0, len(recs)) 123 | } 124 | 125 | func TestClient_UpdateReceiver(t *testing.T) { 126 | client, fsClient, _ := newTestClient() 127 | err := client.UpdateReceiver(testNID, "slack", &config.Receiver{Name: "slack"}) 128 | fsClient.AssertCalled(t, "WriteFile", "test/alertmanager.yml", mock.Anything, mock.Anything) 129 | assert.NoError(t, err) 130 | 131 | err = client.UpdateReceiver(testNID, "nonexistent", &config.Receiver{Name: "nonexistent"}) 132 | fsClient.AssertNumberOfCalls(t, "WriteFile", 1) 133 | assert.Error(t, err) 134 | } 135 | 136 | func TestClient_DeleteReceiver(t *testing.T) { 137 | client, fsClient, _ := newTestClient() 138 | err := client.DeleteReceiver(testNID, "slack") 139 | fsClient.AssertCalled(t, "WriteFile", "test/alertmanager.yml", mock.Anything, mock.Anything) 140 | assert.NoError(t, err) 141 | 142 | err = client.DeleteReceiver(testNID, "nonexistent") 143 | assert.Error(t, err) 144 | fsClient.AssertNumberOfCalls(t, "WriteFile", 1) 145 | } 146 | 147 | func TestClient_ModifyTenantRoute(t *testing.T) { 148 | client, fsClient, _ := newTestClient() 149 | err := client.ModifyTenantRoute(testNID, &config.Route{ 150 | Receiver: "test_tenant_base_route", 151 | Routes: []*config.Route{ 152 | {Receiver: "slack"}, 153 | }, 154 | }) 155 | assert.NoError(t, err) 156 | fsClient.AssertCalled(t, "WriteFile", "test/alertmanager.yml", mock.Anything, mock.Anything) 157 | 158 | err = client.ModifyTenantRoute(testNID, &config.Route{ 159 | Receiver: "invalid_base_route", 160 | Routes: []*config.Route{ 161 | {Receiver: "slack"}, 162 | }, 163 | }) 164 | assert.EqualError(t, err, "route base receiver is incorrect (should be \"test_tenant_base_route\"). The base node should match nothing, then add routes as children of the base node") 165 | 166 | err = client.ModifyTenantRoute(testNID, &config.Route{ 167 | Receiver: "test", 168 | Routes: []*config.Route{{ 169 | Receiver: "nonexistent", 170 | }}, 171 | }) 172 | assert.Error(t, err) 173 | fsClient.AssertNumberOfCalls(t, "WriteFile", 1) 174 | } 175 | 176 | func TestClient_GetRoute(t *testing.T) { 177 | client, _, _ := newTestClient() 178 | 179 | route, err := client.GetRoute(otherNID) 180 | assert.NoError(t, err) 181 | assert.Equal(t, config.Route{Receiver: "other_tenant_base_route", Match: map[string]string{"tenantID": "other"}}, *route) 182 | 183 | _, err = client.GetRoute("no-network") 184 | assert.Error(t, err) 185 | } 186 | 187 | func TestClient_GetTenants(t *testing.T) { 188 | client, _, _ := newTestClient() 189 | 190 | tenants, err := client.GetTenants() 191 | assert.NoError(t, err) 192 | assert.Equal(t, []string{"other", "sample"}, tenants) 193 | } 194 | 195 | func TestClient_GetTemplateFileList(t *testing.T) { 196 | client, _, _ := newTestClient() 197 | 198 | tmpls, err := client.GetTemplateFileList() 199 | assert.NoError(t, err) 200 | assert.Equal(t, []string{"path/to/file1", "path/to/file2", "path/to/file3"}, tmpls) 201 | } 202 | 203 | func TestClient_AddTemplateFile(t *testing.T) { 204 | client, _, out := newTestClient() 205 | newPathName := "path/to/newFile" 206 | 207 | err := client.AddTemplateFile(newPathName) 208 | assert.NoError(t, err) 209 | 210 | newConf, _ := byteToConfig(*out) 211 | assert.Equal(t, len(newConf.Templates), 4) 212 | assert.Equal(t, newPathName, newConf.Templates[3]) 213 | } 214 | 215 | func TestClient_RemoveTemplateFile(t *testing.T) { 216 | client, fsClient, out := newTestClient() 217 | 218 | // Remove existing path 219 | err := client.RemoveTemplateFile("path/to/file1") 220 | assert.NoError(t, err) 221 | 222 | newConf, _ := byteToConfig(*out) 223 | assert.Equal(t, len(newConf.Templates), 2) 224 | fsClient.AssertNumberOfCalls(t, "WriteFile", 1) 225 | 226 | // Remove non-existent path 227 | err = client.RemoveTemplateFile("path/to/noFile") 228 | assert.EqualError(t, err, "path not found: path/to/noFile") 229 | fsClient.AssertNumberOfCalls(t, "WriteFile", 1) 230 | } 231 | 232 | func newTestClient() (AlertmanagerClient, *mocks.FSClient, *[]byte) { 233 | fsClient := &mocks.FSClient{} 234 | fsClient.On("ReadFile", mock.Anything).Return([]byte(testAlertmanagerFile), nil) 235 | 236 | var outputFile []byte 237 | fsClient.On("WriteFile", mock.Anything, mock.Anything, mock.Anything). 238 | Return(nil). 239 | Run(func(args mock.Arguments) { outputFile = args[1].([]byte) }) 240 | tenancy := &alert.TenancyConfig{ 241 | RestrictorLabel: "tenantID", 242 | } 243 | conf := ClientConfig{ 244 | ConfigPath: "test/alertmanager.yml", 245 | AlertmanagerURL: "alertmanager-host:9093", 246 | FsClient: fsClient, 247 | Tenancy: tenancy, 248 | DeleteRoutes: false, 249 | } 250 | return NewClient(conf), fsClient, &outputFile 251 | } 252 | 253 | func byteToConfig(in []byte) (config.Config, error) { 254 | conf := config.Config{} 255 | return conf, yaml.Unmarshal(in, &conf) 256 | } 257 | -------------------------------------------------------------------------------- /alertmanager/client/mocks/AlertmanagerClient.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | // Code generated by mockery v1.0.0. DO NOT EDIT. 9 | 10 | package mocks 11 | 12 | import ( 13 | alert "github.com/facebookincubator/prometheus-configmanager/prometheus/alert" 14 | 15 | config "github.com/facebookincubator/prometheus-configmanager/alertmanager/config" 16 | 17 | mock "github.com/stretchr/testify/mock" 18 | ) 19 | 20 | // AlertmanagerClient is an autogenerated mock type for the AlertmanagerClient type 21 | type AlertmanagerClient struct { 22 | mock.Mock 23 | } 24 | 25 | // AddTemplateFile provides a mock function with given fields: path 26 | func (_m *AlertmanagerClient) AddTemplateFile(path string) error { 27 | ret := _m.Called(path) 28 | 29 | var r0 error 30 | if rf, ok := ret.Get(0).(func(string) error); ok { 31 | r0 = rf(path) 32 | } else { 33 | r0 = ret.Error(0) 34 | } 35 | 36 | return r0 37 | } 38 | 39 | // CreateReceiver provides a mock function with given fields: tenantID, rec 40 | func (_m *AlertmanagerClient) CreateReceiver(tenantID string, rec config.Receiver) error { 41 | ret := _m.Called(tenantID, rec) 42 | 43 | var r0 error 44 | if rf, ok := ret.Get(0).(func(string, config.Receiver) error); ok { 45 | r0 = rf(tenantID, rec) 46 | } else { 47 | r0 = ret.Error(0) 48 | } 49 | 50 | return r0 51 | } 52 | 53 | // DeleteReceiver provides a mock function with given fields: tenantID, receiverName 54 | func (_m *AlertmanagerClient) DeleteReceiver(tenantID string, receiverName string) error { 55 | ret := _m.Called(tenantID, receiverName) 56 | 57 | var r0 error 58 | if rf, ok := ret.Get(0).(func(string, string) error); ok { 59 | r0 = rf(tenantID, receiverName) 60 | } else { 61 | r0 = ret.Error(0) 62 | } 63 | 64 | return r0 65 | } 66 | 67 | // GetGlobalConfig provides a mock function with given fields: 68 | func (_m *AlertmanagerClient) GetGlobalConfig() (*config.GlobalConfig, error) { 69 | ret := _m.Called() 70 | 71 | var r0 *config.GlobalConfig 72 | if rf, ok := ret.Get(0).(func() *config.GlobalConfig); ok { 73 | r0 = rf() 74 | } else { 75 | if ret.Get(0) != nil { 76 | r0 = ret.Get(0).(*config.GlobalConfig) 77 | } 78 | } 79 | 80 | var r1 error 81 | if rf, ok := ret.Get(1).(func() error); ok { 82 | r1 = rf() 83 | } else { 84 | r1 = ret.Error(1) 85 | } 86 | 87 | return r0, r1 88 | } 89 | 90 | // GetReceivers provides a mock function with given fields: tenantID 91 | func (_m *AlertmanagerClient) GetReceivers(tenantID string) ([]config.Receiver, error) { 92 | ret := _m.Called(tenantID) 93 | 94 | var r0 []config.Receiver 95 | if rf, ok := ret.Get(0).(func(string) []config.Receiver); ok { 96 | r0 = rf(tenantID) 97 | } else { 98 | if ret.Get(0) != nil { 99 | r0 = ret.Get(0).([]config.Receiver) 100 | } 101 | } 102 | 103 | var r1 error 104 | if rf, ok := ret.Get(1).(func(string) error); ok { 105 | r1 = rf(tenantID) 106 | } else { 107 | r1 = ret.Error(1) 108 | } 109 | 110 | return r0, r1 111 | } 112 | 113 | // GetRoute provides a mock function with given fields: tenantID 114 | func (_m *AlertmanagerClient) GetRoute(tenantID string) (*config.Route, error) { 115 | ret := _m.Called(tenantID) 116 | 117 | var r0 *config.Route 118 | if rf, ok := ret.Get(0).(func(string) *config.Route); ok { 119 | r0 = rf(tenantID) 120 | } else { 121 | if ret.Get(0) != nil { 122 | r0 = ret.Get(0).(*config.Route) 123 | } 124 | } 125 | 126 | var r1 error 127 | if rf, ok := ret.Get(1).(func(string) error); ok { 128 | r1 = rf(tenantID) 129 | } else { 130 | r1 = ret.Error(1) 131 | } 132 | 133 | return r0, r1 134 | } 135 | 136 | // GetTemplateFileList provides a mock function with given fields: 137 | func (_m *AlertmanagerClient) GetTemplateFileList() ([]string, error) { 138 | ret := _m.Called() 139 | 140 | var r0 []string 141 | if rf, ok := ret.Get(0).(func() []string); ok { 142 | r0 = rf() 143 | } else { 144 | if ret.Get(0) != nil { 145 | r0 = ret.Get(0).([]string) 146 | } 147 | } 148 | 149 | var r1 error 150 | if rf, ok := ret.Get(1).(func() error); ok { 151 | r1 = rf() 152 | } else { 153 | r1 = ret.Error(1) 154 | } 155 | 156 | return r0, r1 157 | } 158 | 159 | // GetTenants provides a mock function with given fields: 160 | func (_m *AlertmanagerClient) GetTenants() ([]string, error) { 161 | ret := _m.Called() 162 | 163 | var r0 []string 164 | if rf, ok := ret.Get(0).(func() []string); ok { 165 | r0 = rf() 166 | } else { 167 | if ret.Get(0) != nil { 168 | r0 = ret.Get(0).([]string) 169 | } 170 | } 171 | 172 | var r1 error 173 | if rf, ok := ret.Get(1).(func() error); ok { 174 | r1 = rf() 175 | } else { 176 | r1 = ret.Error(1) 177 | } 178 | 179 | return r0, r1 180 | } 181 | 182 | // ModifyTenantRoute provides a mock function with given fields: tenantID, route 183 | func (_m *AlertmanagerClient) ModifyTenantRoute(tenantID string, route *config.Route) error { 184 | ret := _m.Called(tenantID, route) 185 | 186 | var r0 error 187 | if rf, ok := ret.Get(0).(func(string, *config.Route) error); ok { 188 | r0 = rf(tenantID, route) 189 | } else { 190 | r0 = ret.Error(0) 191 | } 192 | 193 | return r0 194 | } 195 | 196 | // ReloadAlertmanager provides a mock function with given fields: 197 | func (_m *AlertmanagerClient) ReloadAlertmanager() error { 198 | ret := _m.Called() 199 | 200 | var r0 error 201 | if rf, ok := ret.Get(0).(func() error); ok { 202 | r0 = rf() 203 | } else { 204 | r0 = ret.Error(0) 205 | } 206 | 207 | return r0 208 | } 209 | 210 | // RemoveTemplateFile provides a mock function with given fields: path 211 | func (_m *AlertmanagerClient) RemoveTemplateFile(path string) error { 212 | ret := _m.Called(path) 213 | 214 | var r0 error 215 | if rf, ok := ret.Get(0).(func(string) error); ok { 216 | r0 = rf(path) 217 | } else { 218 | r0 = ret.Error(0) 219 | } 220 | 221 | return r0 222 | } 223 | 224 | // SetGlobalConfig provides a mock function with given fields: globalConfig 225 | func (_m *AlertmanagerClient) SetGlobalConfig(globalConfig config.GlobalConfig) error { 226 | ret := _m.Called(globalConfig) 227 | 228 | var r0 error 229 | if rf, ok := ret.Get(0).(func(config.GlobalConfig) error); ok { 230 | r0 = rf(globalConfig) 231 | } else { 232 | r0 = ret.Error(0) 233 | } 234 | 235 | return r0 236 | } 237 | 238 | // Tenancy provides a mock function with given fields: 239 | func (_m *AlertmanagerClient) Tenancy() *alert.TenancyConfig { 240 | ret := _m.Called() 241 | 242 | var r0 *alert.TenancyConfig 243 | if rf, ok := ret.Get(0).(func() *alert.TenancyConfig); ok { 244 | r0 = rf() 245 | } else { 246 | if ret.Get(0) != nil { 247 | r0 = ret.Get(0).(*alert.TenancyConfig) 248 | } 249 | } 250 | 251 | return r0 252 | } 253 | 254 | // UpdateReceiver provides a mock function with given fields: tenantID, receiverName, newRec 255 | func (_m *AlertmanagerClient) UpdateReceiver(tenantID string, receiverName string, newRec *config.Receiver) error { 256 | ret := _m.Called(tenantID, receiverName, newRec) 257 | 258 | var r0 error 259 | if rf, ok := ret.Get(0).(func(string, string, *config.Receiver) error); ok { 260 | r0 = rf(tenantID, receiverName, newRec) 261 | } else { 262 | r0 = ret.Error(0) 263 | } 264 | 265 | return r0 266 | } 267 | -------------------------------------------------------------------------------- /alertmanager/client/mocks/TemplateClient.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | // Code generated by mockery v1.0.0. DO NOT EDIT. 9 | 10 | package mocks 11 | 12 | import mock "github.com/stretchr/testify/mock" 13 | 14 | // TemplateClient is an autogenerated mock type for the TemplateClient type 15 | type TemplateClient struct { 16 | mock.Mock 17 | } 18 | 19 | // AddTemplate provides a mock function with given fields: filename, tmplName, tmplText 20 | func (_m *TemplateClient) AddTemplate(filename string, tmplName string, tmplText string) error { 21 | ret := _m.Called(filename, tmplName, tmplText) 22 | 23 | var r0 error 24 | if rf, ok := ret.Get(0).(func(string, string, string) error); ok { 25 | r0 = rf(filename, tmplName, tmplText) 26 | } else { 27 | r0 = ret.Error(0) 28 | } 29 | 30 | return r0 31 | } 32 | 33 | // CreateTemplateFile provides a mock function with given fields: filename, fileText 34 | func (_m *TemplateClient) CreateTemplateFile(filename string, fileText string) error { 35 | ret := _m.Called(filename, fileText) 36 | 37 | var r0 error 38 | if rf, ok := ret.Get(0).(func(string, string) error); ok { 39 | r0 = rf(filename, fileText) 40 | } else { 41 | r0 = ret.Error(0) 42 | } 43 | 44 | return r0 45 | } 46 | 47 | // DeleteTemplate provides a mock function with given fields: filename, tmplName 48 | func (_m *TemplateClient) DeleteTemplate(filename string, tmplName string) error { 49 | ret := _m.Called(filename, tmplName) 50 | 51 | var r0 error 52 | if rf, ok := ret.Get(0).(func(string, string) error); ok { 53 | r0 = rf(filename, tmplName) 54 | } else { 55 | r0 = ret.Error(0) 56 | } 57 | 58 | return r0 59 | } 60 | 61 | // DeleteTemplateFile provides a mock function with given fields: filename 62 | func (_m *TemplateClient) DeleteTemplateFile(filename string) error { 63 | ret := _m.Called(filename) 64 | 65 | var r0 error 66 | if rf, ok := ret.Get(0).(func(string) error); ok { 67 | r0 = rf(filename) 68 | } else { 69 | r0 = ret.Error(0) 70 | } 71 | 72 | return r0 73 | } 74 | 75 | // EditTemplate provides a mock function with given fields: filename, tmplName, tmplText 76 | func (_m *TemplateClient) EditTemplate(filename string, tmplName string, tmplText string) error { 77 | ret := _m.Called(filename, tmplName, tmplText) 78 | 79 | var r0 error 80 | if rf, ok := ret.Get(0).(func(string, string, string) error); ok { 81 | r0 = rf(filename, tmplName, tmplText) 82 | } else { 83 | r0 = ret.Error(0) 84 | } 85 | 86 | return r0 87 | } 88 | 89 | // EditTemplateFile provides a mock function with given fields: filename, fileText 90 | func (_m *TemplateClient) EditTemplateFile(filename string, fileText string) error { 91 | ret := _m.Called(filename, fileText) 92 | 93 | var r0 error 94 | if rf, ok := ret.Get(0).(func(string, string) error); ok { 95 | r0 = rf(filename, fileText) 96 | } else { 97 | r0 = ret.Error(0) 98 | } 99 | 100 | return r0 101 | } 102 | 103 | // GetTemplate provides a mock function with given fields: filename, tmplName 104 | func (_m *TemplateClient) GetTemplate(filename string, tmplName string) (string, error) { 105 | ret := _m.Called(filename, tmplName) 106 | 107 | var r0 string 108 | if rf, ok := ret.Get(0).(func(string, string) string); ok { 109 | r0 = rf(filename, tmplName) 110 | } else { 111 | r0 = ret.Get(0).(string) 112 | } 113 | 114 | var r1 error 115 | if rf, ok := ret.Get(1).(func(string, string) error); ok { 116 | r1 = rf(filename, tmplName) 117 | } else { 118 | r1 = ret.Error(1) 119 | } 120 | 121 | return r0, r1 122 | } 123 | 124 | // GetTemplateFile provides a mock function with given fields: filename 125 | func (_m *TemplateClient) GetTemplateFile(filename string) (string, error) { 126 | ret := _m.Called(filename) 127 | 128 | var r0 string 129 | if rf, ok := ret.Get(0).(func(string) string); ok { 130 | r0 = rf(filename) 131 | } else { 132 | r0 = ret.Get(0).(string) 133 | } 134 | 135 | var r1 error 136 | if rf, ok := ret.Get(1).(func(string) error); ok { 137 | r1 = rf(filename) 138 | } else { 139 | r1 = ret.Error(1) 140 | } 141 | 142 | return r0, r1 143 | } 144 | 145 | // GetTemplates provides a mock function with given fields: filename 146 | func (_m *TemplateClient) GetTemplates(filename string) (map[string]string, error) { 147 | ret := _m.Called(filename) 148 | 149 | var r0 map[string]string 150 | if rf, ok := ret.Get(0).(func(string) map[string]string); ok { 151 | r0 = rf(filename) 152 | } else { 153 | if ret.Get(0) != nil { 154 | r0 = ret.Get(0).(map[string]string) 155 | } 156 | } 157 | 158 | var r1 error 159 | if rf, ok := ret.Get(1).(func(string) error); ok { 160 | r1 = rf(filename) 161 | } else { 162 | r1 = ret.Error(1) 163 | } 164 | 165 | return r0, r1 166 | } 167 | 168 | // Root provides a mock function with given fields: 169 | func (_m *TemplateClient) Root() string { 170 | ret := _m.Called() 171 | 172 | var r0 string 173 | if rf, ok := ret.Get(0).(func() string); ok { 174 | r0 = rf() 175 | } else { 176 | r0 = ret.Get(0).(string) 177 | } 178 | 179 | return r0 180 | } 181 | -------------------------------------------------------------------------------- /alertmanager/client/template_client.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package client 9 | 10 | import ( 11 | "fmt" 12 | "reflect" 13 | "sort" 14 | "strings" 15 | "unsafe" 16 | 17 | "text/template" 18 | 19 | "github.com/facebookincubator/prometheus-configmanager/fsclient" 20 | "github.com/facebookincubator/prometheus-configmanager/prometheus/alert" 21 | "github.com/thoas/go-funk" 22 | ) 23 | 24 | const TemplateFilePostfix = ".tmpl" 25 | 26 | // TemplateClient interface provides methods for modifying template files 27 | // and individual templates within them 28 | type TemplateClient interface { 29 | GetTemplateFile(filename string) (string, error) 30 | CreateTemplateFile(filename, fileText string) error 31 | EditTemplateFile(filename, fileText string) error 32 | DeleteTemplateFile(filename string) error 33 | 34 | GetTemplates(filename string) (map[string]string, error) 35 | 36 | GetTemplate(filename, tmplName string) (string, error) 37 | AddTemplate(filename, tmplName, tmplText string) error 38 | EditTemplate(filename, tmplName, tmplText string) error 39 | DeleteTemplate(filename, tmplName string) error 40 | 41 | Root() string 42 | } 43 | 44 | func NewTemplateClient(fsClient fsclient.FSClient, fileLocks *alert.FileLocker) TemplateClient { 45 | return &templateClient{ 46 | fsClient: fsClient, 47 | fileLocks: fileLocks, 48 | } 49 | } 50 | 51 | type templateClient struct { 52 | fsClient fsclient.FSClient 53 | fileLocks *alert.FileLocker 54 | } 55 | 56 | func (t *templateClient) GetTemplateFile(filename string) (string, error) { 57 | t.fileLocks.RLock(filename) 58 | defer t.fileLocks.RUnlock(filename) 59 | 60 | file, err := t.fsClient.ReadFile(addFilePostfix(filename)) 61 | if err != nil { 62 | return "", err 63 | } 64 | 65 | return string(file), nil 66 | } 67 | 68 | func (t *templateClient) CreateTemplateFile(filename, fileText string) error { 69 | t.fileLocks.Lock(filename) 70 | defer t.fileLocks.Unlock(filename) 71 | 72 | return t.fsClient.WriteFile(addFilePostfix(filename), []byte(fileText), 0660) 73 | } 74 | 75 | func (t *templateClient) EditTemplateFile(filename, fileText string) error { 76 | t.fileLocks.Lock(filename) 77 | defer t.fileLocks.Unlock(filename) 78 | 79 | return t.fsClient.WriteFile(addFilePostfix(filename), []byte(fileText), 0660) 80 | } 81 | 82 | func (t *templateClient) DeleteTemplateFile(filename string) error { 83 | t.fileLocks.Lock(filename) 84 | defer t.fileLocks.Unlock(filename) 85 | 86 | return t.fsClient.DeleteFile(addFilePostfix(filename)) 87 | } 88 | 89 | func (t *templateClient) GetTemplates(filename string) (map[string]string, error) { 90 | t.fileLocks.RLock(filename) 91 | defer t.fileLocks.RUnlock(filename) 92 | 93 | tmpl, err := t.readTmplFile(filename) 94 | if err != nil { 95 | return nil, err 96 | } 97 | 98 | tmplMap := getTemplatesByName(tmpl) 99 | 100 | tmplTextMap := make(map[string]string, len(tmplMap)) 101 | for key, t := range tmplMap { 102 | // Don't include template for entire file 103 | if t.Name() == t.ParseName { 104 | continue 105 | } 106 | tmplTextMap[key] = writeTemplateText(t) 107 | } 108 | return tmplTextMap, nil 109 | } 110 | 111 | func (t *templateClient) GetTemplate(filename, tmplName string) (string, error) { 112 | t.fileLocks.RLock(filename) 113 | defer t.fileLocks.RUnlock(filename) 114 | 115 | tmplFile, err := t.readTmplFile(filename) 116 | if err != nil { 117 | return "", err 118 | } 119 | tmplMap := getTemplatesByName(tmplFile) 120 | 121 | tmpl := tmplMap[tmplName] 122 | if tmpl == nil { 123 | return "", fmt.Errorf("template %s not found", tmplName) 124 | } 125 | 126 | return writeTemplateText(tmpl), nil 127 | } 128 | 129 | func (t *templateClient) AddTemplate(filename, tmplName, tmplText string) error { 130 | t.fileLocks.Lock(filename) 131 | defer t.fileLocks.Unlock(filename) 132 | 133 | tmplFile, err := t.readTmplFile(filename) 134 | if err != nil { 135 | return err 136 | } 137 | tmplMap := getTemplatesByName(tmplFile) 138 | 139 | if tmplMap[tmplName] != nil { 140 | return fmt.Errorf("template %s already exists", tmplName) 141 | } 142 | 143 | newTmpl := &template.Template{} 144 | newTmplBody, err := newTmpl.Parse(tmplText) 145 | if err != nil { 146 | return fmt.Errorf("error parsing template: %v", err) 147 | } 148 | tmplMap[tmplName] = newTmplBody 149 | 150 | return t.writeTmplFile(filename, writeTmplMapText(tmplMap)) 151 | } 152 | 153 | func (t *templateClient) EditTemplate(filename, tmplName, tmplText string) error { 154 | t.fileLocks.Lock(filename) 155 | defer t.fileLocks.Unlock(filename) 156 | 157 | tmplFile, err := t.readTmplFile(filename) 158 | if err != nil { 159 | return err 160 | } 161 | tmplMap := getTemplatesByName(tmplFile) 162 | 163 | if tmplMap[tmplName] == nil { 164 | return fmt.Errorf("template %s does not exist", tmplName) 165 | } 166 | 167 | parseTmpl := &template.Template{} 168 | newTmpl, err := parseTmpl.Parse(tmplText) 169 | if err != nil { 170 | return fmt.Errorf("error adding template: %v", err) 171 | } 172 | tmplMap[tmplName] = newTmpl 173 | 174 | return t.writeTmplFile(filename, writeTmplMapText(tmplMap)) 175 | } 176 | 177 | func (t *templateClient) DeleteTemplate(filename, tmplName string) error { 178 | t.fileLocks.Lock(filename) 179 | defer t.fileLocks.Unlock(filename) 180 | 181 | tmplFile, err := t.readTmplFile(filename) 182 | if err != nil { 183 | return err 184 | } 185 | tmplMap := getTemplatesByName(tmplFile) 186 | 187 | if tmplMap[tmplName] == nil { 188 | return fmt.Errorf("template %s does not exist", tmplName) 189 | } 190 | 191 | delete(tmplMap, tmplName) 192 | 193 | return t.writeTmplFile(filename, writeTmplMapText(tmplMap)) 194 | } 195 | 196 | func (t *templateClient) Root() string { 197 | return t.fsClient.Root() 198 | } 199 | 200 | func (t *templateClient) writeTmplFile(filename, text string) error { 201 | err := t.fsClient.WriteFile(addFilePostfix(filename), []byte(text), 0660) 202 | if err != nil { 203 | return fmt.Errorf("error writing template file: %v", err) 204 | } 205 | return nil 206 | } 207 | 208 | func (t *templateClient) readTmplFile(filename string) (*template.Template, error) { 209 | tmplFile, err := template.ParseFiles(t.Root() + addFilePostfix(filename)) 210 | if err != nil { 211 | return nil, fmt.Errorf("error parsing template files: %v", err) 212 | } 213 | return tmplFile, nil 214 | } 215 | 216 | func addFilePostfix(filename string) string { 217 | return filename + TemplateFilePostfix 218 | } 219 | 220 | func writeTemplateText(tmpl *template.Template) string { 221 | return tmpl.Root.String() 222 | } 223 | 224 | func writeTmplMapText(tmplMap map[string]*template.Template) string { 225 | str := strings.Builder{} 226 | // Sort names for consistency 227 | names := funk.Keys(tmplMap).([]string) 228 | sort.Strings(names) 229 | 230 | for _, name := range names { 231 | tmpl := tmplMap[name] 232 | if name == tmpl.Tree.ParseName { 233 | continue 234 | } 235 | str.WriteString(defineTemplate(name, tmpl.Root.String())) 236 | str.WriteRune('\n') 237 | } 238 | return str.String() 239 | } 240 | 241 | func defineTemplate(tmplName, tmplText string) string { 242 | return fmt.Sprintf(`{{ define "%s" }}%s{{ end }}`, tmplName, tmplText) 243 | } 244 | 245 | func getTemplatesByName(tmpl *template.Template) map[string]*template.Template { 246 | field := reflect.ValueOf(tmpl).Elem().FieldByName("tmpl") 247 | return reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).Elem().Interface().(map[string]*template.Template) 248 | } 249 | -------------------------------------------------------------------------------- /alertmanager/client/template_client_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package client 9 | 10 | import ( 11 | "io/ioutil" 12 | "strings" 13 | "testing" 14 | 15 | "github.com/facebookincubator/prometheus-configmanager/fsclient/mocks" 16 | "github.com/facebookincubator/prometheus-configmanager/prometheus/alert" 17 | "github.com/stretchr/testify/assert" 18 | "github.com/stretchr/testify/mock" 19 | ) 20 | 21 | func TestTemplateClient_GetTemplateFile(t *testing.T) { 22 | client, _, _ := newTestTmplClient() 23 | 24 | fileText, err := client.GetTemplateFile("") 25 | assert.NoError(t, err) 26 | origFile, _ := readTestFileString() 27 | assert.Equal(t, origFile, fileText) 28 | } 29 | 30 | func TestTemplateClient_CreateTemplateFile(t *testing.T) { 31 | client, _, _ := newTestTmplClient() 32 | 33 | err := client.CreateTemplateFile("test", "text") 34 | assert.NoError(t, err) 35 | } 36 | 37 | func TestTemplateClient_EditTemplateFile(t *testing.T) { 38 | client, _, out := newTestTmplClient() 39 | 40 | err := client.EditTemplateFile("test", "text") 41 | assert.NoError(t, err) 42 | assert.Equal(t, "text", string(*out)) 43 | } 44 | 45 | func TestTemplateClient_DeleteTemplateFile(t *testing.T) { 46 | client, _, _ := newTestTmplClient() 47 | 48 | err := client.DeleteTemplateFile("test") 49 | assert.NoError(t, err) 50 | } 51 | 52 | func TestTemplateClient_GetTemplates(t *testing.T) { 53 | client, _, _ := newTestTmplClient() 54 | 55 | tmpls, err := client.GetTemplates("test") 56 | assert.NoError(t, err) 57 | 58 | assert.Len(t, tmpls, 2) 59 | assert.NotNil(t, tmpls["slack.myorg.text"]) 60 | assert.NotNil(t, tmpls["slack.myorg2.text"]) 61 | } 62 | 63 | func TestTemplateClient_GetTemplate(t *testing.T) { 64 | client, _, _ := newTestTmplClient() 65 | 66 | const expectedText = `https://internal.myorg.net/wiki/alerts/{{.GroupLabels.app}}/{{.GroupLabels.alertname}}` 67 | 68 | text, err := client.GetTemplate("test", "slack.myorg.text") 69 | assert.NoError(t, err) 70 | assert.Equal(t, expectedText, text) 71 | 72 | _, err = client.GetTemplate("test", "noTemplate") 73 | assert.EqualError(t, err, "template noTemplate not found") 74 | } 75 | 76 | func TestTemplateClient_AddTemplate(t *testing.T) { 77 | client, _, out := newTestTmplClient() 78 | 79 | err := client.AddTemplate("test", "slack2", "test slack body") 80 | assert.NoError(t, err) 81 | origFile, _ := readTestFileString() 82 | expectedOutput := origFile + ` 83 | {{ define "slack2" }}test slack body{{ end }} 84 | ` 85 | assert.Equal(t, expectedOutput, string(*out)) 86 | } 87 | 88 | func TestTemplateClient_EditTemplate(t *testing.T) { 89 | client, _, out := newTestTmplClient() 90 | 91 | err := client.EditTemplate("test", "slack.myorg.text", "new text") 92 | 93 | expectedText := `{{ define "slack.myorg.text" }}new text{{ end }} 94 | {{ define "slack.myorg2.text" }}https://external.myorg.net/wiki/alerts/{{.GroupLabels.app}}/{{.GroupLabels.alertname}}{{ end }} 95 | ` 96 | assert.NoError(t, err) 97 | assert.Equal(t, expectedText, string(*out)) 98 | } 99 | 100 | func TestTemplateClient_DeleteTemplate(t *testing.T) { 101 | client, _, out := newTestTmplClient() 102 | 103 | err := client.DeleteTemplate("test", "slack.myorg.text") 104 | assert.NoError(t, err) 105 | 106 | testFile, err := readTestFileString() 107 | assert.NoError(t, err) 108 | // Expected to remove first definition 109 | expectedText := testFile[strings.Index(testFile, `{{ define "slack.myorg2.text" }}`):] 110 | assert.Equal(t, expectedText, strings.TrimSpace(string(*out))) 111 | 112 | err = client.DeleteTemplate("test", "notATemplate") 113 | assert.EqualError(t, err, "template notATemplate does not exist") 114 | } 115 | 116 | func newTestTmplClient() (TemplateClient, *mocks.FSClient, *[]byte) { 117 | fsClient := &mocks.FSClient{} 118 | fsClient.On("ReadFile", mock.Anything).Return(readTestFile()) 119 | 120 | var outputFile []byte 121 | fsClient.On("WriteFile", mock.Anything, mock.Anything, mock.Anything). 122 | Return(nil). 123 | Run(func(args mock.Arguments) { outputFile = args[1].([]byte) }) 124 | 125 | fsClient.On("DeleteFile", mock.Anything).Return(nil) 126 | fsClient.On("Root").Return("testdata/") 127 | fileLocks, _ := alert.NewFileLocker(alert.NewDirectoryClient(".")) 128 | return NewTemplateClient(fsClient, fileLocks), fsClient, &outputFile 129 | } 130 | 131 | const copyrightHeaderLength = 200 132 | 133 | func readTestFile() ([]byte, error) { 134 | text, err := ioutil.ReadFile("testdata/test.tmpl") 135 | return text[copyrightHeaderLength:], err 136 | } 137 | 138 | func readTestFileString() (string, error) { 139 | file, err := ioutil.ReadFile("testdata/test.tmpl") 140 | return string(file[copyrightHeaderLength:]), err 141 | } 142 | -------------------------------------------------------------------------------- /alertmanager/client/testdata/test.tmpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */}} 7 | 8 | {{ define "slack.myorg.text" }}https://internal.myorg.net/wiki/alerts/{{.GroupLabels.app}}/{{.GroupLabels.alertname}}{{ end }} 9 | {{ define "slack.myorg2.text" }}https://external.myorg.net/wiki/alerts/{{.GroupLabels.app}}/{{.GroupLabels.alertname}}{{ end }} -------------------------------------------------------------------------------- /alertmanager/common/config.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package common 9 | 10 | // HTTPConfig is a copy of prometheus/common/config.HTTPClientConfig with 11 | // `Secret` fields replaced with strings to enable marshaling without obfuscation 12 | type HTTPConfig struct { 13 | // The HTTP basic authentication credentials for the targets. 14 | BasicAuth *BasicAuth `yaml:"basic_auth,omitempty" json:"basic_auth,omitempty"` 15 | // The bearer token for the targets. 16 | BearerToken string `yaml:"bearer_token,omitempty" json:"bearer_token,omitempty"` 17 | // The bearer token file for the targets. 18 | 19 | // TODO: Support file storage 20 | //BearerTokenFile string `yaml:"bearer_token_file,omitempty"` 21 | // HTTP proxy server to use to connect to the targets. 22 | ProxyURL string `yaml:"proxy_url,omitempty" json:"proxy_url,omitempty"` 23 | 24 | // TLSConfig to use to connect to the targets. 25 | TLSConfig TLSConfig `yaml:"tls_config,omitempty" json:"tls_config,omitempty"` 26 | } 27 | 28 | // BasicAuth is a copy of prometheus/common/config.BasicAuth with `Secret` 29 | // fields replaced with strings to enable marshaling without obfuscation 30 | type BasicAuth struct { 31 | Username string `yaml:"username" json:"username"` 32 | Password string `yaml:"password,omitempty" json:"password,omitempty"` 33 | 34 | // TODO: Support file storage 35 | //PasswordFile string `yaml:"password_file,omitempty"` 36 | } 37 | 38 | // TLSConfig is a copy of prometheus/common/config.TLSConfig without file fields 39 | // since storing files is not supported by alertmanager-configurer yet 40 | type TLSConfig struct { 41 | // TODO: Support file storage 42 | //// The CA cert to use for the targets. 43 | //CAFile string `yaml:"ca_file,omitempty"` 44 | //// The client cert file for the targets. 45 | //CertFile string `yaml:"cert_file,omitempty"` 46 | ////The client key file for the targets. 47 | //KeyFile string `yaml:"key_file,omitempty"` 48 | 49 | // Used to verify the hostname for the targets. 50 | ServerName string `yaml:"server_name,omitempty" json:"server_name,omitempty"` 51 | // Disable target certificate validation. 52 | InsecureSkipVerify bool `yaml:"insecure_skip_verify" json:"insecure_skip_verify,omitempty"` 53 | } 54 | -------------------------------------------------------------------------------- /alertmanager/config/config.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package config 9 | 10 | import ( 11 | "fmt" 12 | "time" 13 | 14 | "github.com/facebookincubator/prometheus-configmanager/alertmanager/common" 15 | "github.com/thoas/go-funk" 16 | 17 | amconfig "github.com/prometheus/alertmanager/config" 18 | "github.com/prometheus/common/model" 19 | "gopkg.in/yaml.v2" 20 | ) 21 | 22 | const ( 23 | TenantBaseRoutePostfix = "tenant_base_route" 24 | ) 25 | 26 | // Config uses a custom receiver struct to avoid scrubbing of 'secrets' during 27 | // marshaling 28 | type Config struct { 29 | Global *GlobalConfig `yaml:"global,omitempty" json:"global,omitempty"` 30 | Route *Route `yaml:"route,omitempty" json:"route,omitempty"` 31 | InhibitRules []*amconfig.InhibitRule `yaml:"inhibit_rules,omitempty" json:"inhibit_rules,omitempty"` 32 | Receivers []*Receiver `yaml:"receivers,omitempty" json:"receivers,omitempty"` 33 | Templates []string `yaml:"templates" json:"templates"` 34 | } 35 | 36 | // GetReceiver returns the receiver config with the given name 37 | func (c *Config) GetReceiver(name string) *Receiver { 38 | for _, rec := range c.Receivers { 39 | if rec.Name == name { 40 | return rec 41 | } 42 | } 43 | return nil 44 | } 45 | 46 | func (c *Config) GetRouteIdx(name string) int { 47 | for idx, route := range c.Route.Routes { 48 | if route.Receiver == name { 49 | return idx 50 | } 51 | } 52 | return -1 53 | } 54 | 55 | func (c *Config) InitializeNetworkBaseRoute(route *Route, matcherLabel, tenantID string) error { 56 | baseRouteName := MakeBaseRouteName(tenantID) 57 | if c.GetReceiver(baseRouteName) != nil { 58 | return fmt.Errorf("Base route for tenant %s already exists", tenantID) 59 | } 60 | 61 | c.Receivers = append(c.Receivers, &Receiver{Name: baseRouteName}) 62 | route.Receiver = baseRouteName 63 | 64 | if matcherLabel != "" { 65 | route.Match = map[string]string{matcherLabel: tenantID} 66 | } 67 | 68 | c.Route.Routes = append(c.Route.Routes, route) 69 | 70 | return c.Validate() 71 | } 72 | 73 | // Validate makes sure that the config is properly formed. Unmarshal the yaml 74 | // data into an alertmanager Config struct to ensure that it is properly formed 75 | func (c *Config) Validate() error { 76 | yamlData, err := yaml.Marshal(c) 77 | if err != nil { 78 | return err 79 | } 80 | 81 | err = yaml.Unmarshal(yamlData, &amconfig.Config{}) 82 | if err != nil { 83 | return err 84 | } 85 | return nil 86 | } 87 | 88 | func (c *Config) SearchRoutesForReceiver(receiver string) bool { 89 | if c.Route.Receiver == receiver { 90 | return true 91 | } 92 | for _, route := range c.Route.Routes { 93 | if searchRoutesForReceiverImpl(receiver, route) { 94 | return true 95 | } 96 | } 97 | return false 98 | } 99 | 100 | func searchRoutesForReceiverImpl(receiver string, route *Route) bool { 101 | if route.Receiver == receiver { 102 | return true 103 | } 104 | for _, route := range route.Routes { 105 | if searchRoutesForReceiverImpl(receiver, route) { 106 | return true 107 | } 108 | } 109 | return false 110 | } 111 | 112 | func (c *Config) RemoveReceiverFromRoute(receiver string) { 113 | for i, route := range c.Route.Routes { 114 | c.Route.Routes[i] = removeReceiverFromRouteImpl(receiver, route) 115 | } 116 | prunedRoutes := funk.Filter(c.Route.Routes, func(x *Route) bool { return x != nil }) 117 | c.Route.Routes = prunedRoutes.([]*Route) 118 | } 119 | 120 | func removeReceiverFromRouteImpl(receiver string, route *Route) *Route { 121 | if route.Receiver == receiver { 122 | return nil 123 | } 124 | for i, childRoute := range route.Routes { 125 | route.Routes[i] = removeReceiverFromRouteImpl(receiver, childRoute) 126 | } 127 | // Remove nil routes from array 128 | prunedRoutes := funk.Filter(route.Routes, func(x *Route) bool { return x != nil }) 129 | route.Routes = prunedRoutes.([]*Route) 130 | return route 131 | } 132 | 133 | // GlobalConfig is a copy of prometheus/alertmanager/config.GlobalConfig with 134 | // `Secret` fields replaced with strings to enable marshaling without obfuscation 135 | type GlobalConfig struct { 136 | // ResolveTimeout is the time after which an alert is declared resolved 137 | // if it has not been updated. 138 | ResolveTimeout string `yaml:"resolve_timeout" json:"resolve_timeout"` 139 | 140 | HTTPConfig *common.HTTPConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"` 141 | 142 | SMTPFrom string `yaml:"smtp_from,omitempty" json:"smtp_from,omitempty"` 143 | SMTPHello string `yaml:"smtp_hello,omitempty" json:"smtp_hello,omitempty"` 144 | SMTPSmarthost string `yaml:"smtp_smarthost,omitempty" json:"smtp_smarthost,omitempty"` 145 | SMTPAuthUsername string `yaml:"smtp_auth_username,omitempty" json:"smtp_auth_username,omitempty"` 146 | SMTPAuthPassword string `yaml:"smtp_auth_password,omitempty" json:"smtp_auth_password,omitempty"` 147 | SMTPAuthSecret string `yaml:"smtp_auth_secret,omitempty" json:"smtp_auth_secret,omitempty"` 148 | SMTPAuthIdentity string `yaml:"smtp_auth_identity,omitempty" json:"smtp_auth_identity,omitempty"` 149 | SMTPRequireTLS bool `yaml:"smtp_require_tls,omitempty" json:"smtp_require_tls,omitempty"` 150 | SlackAPIURL *amconfig.URL `yaml:"slack_api_url,omitempty" json:"slack_api_url,omitempty"` 151 | PagerdutyURL *amconfig.URL `yaml:"pagerduty_url,omitempty" json:"pagerduty_url,omitempty"` 152 | HipchatAPIURL *amconfig.URL `yaml:"hipchat_api_url,omitempty" json:"hipchat_api_url,omitempty"` 153 | HipchatAuthToken string `yaml:"hipchat_auth_token,omitempty" json:"hipchat_auth_token,omitempty"` 154 | OpsGenieAPIURL *amconfig.URL `yaml:"opsgenie_api_url,omitempty" json:"opsgenie_api_url,omitempty"` 155 | OpsGenieAPIKey string `yaml:"opsgenie_api_key,omitempty" json:"opsgenie_api_key,omitempty"` 156 | WeChatAPIURL *amconfig.URL `yaml:"wechat_api_url,omitempty" json:"wechat_api_url,omitempty"` 157 | WeChatAPISecret string `yaml:"wechat_api_secret,omitempty" json:"wechat_api_secret,omitempty"` 158 | WeChatAPICorpID string `yaml:"wechat_api_corp_id,omitempty" json:"wechat_api_corp_id,omitempty"` 159 | VictorOpsAPIURL *amconfig.URL `yaml:"victorops_api_url,omitempty" json:"victorops_api_url,omitempty"` 160 | VictorOpsAPIKey string `yaml:"victorops_api_key,omitempty" json:"victorops_api_key,omitempty"` 161 | } 162 | 163 | func DefaultGlobalConfig() GlobalConfig { 164 | return GlobalConfig{ 165 | ResolveTimeout: model.Duration(5 * time.Minute).String(), 166 | HTTPConfig: &common.HTTPConfig{}, 167 | 168 | SMTPHello: "localhost", 169 | SMTPRequireTLS: false, 170 | } 171 | } 172 | 173 | func MakeBaseRouteName(tenantID string) string { 174 | return fmt.Sprintf("%s_%s", tenantID, TenantBaseRoutePostfix) 175 | } 176 | -------------------------------------------------------------------------------- /alertmanager/config/config_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package config 9 | 10 | import ( 11 | "encoding/json" 12 | "fmt" 13 | "testing" 14 | 15 | "github.com/stretchr/testify/assert" 16 | ) 17 | 18 | var ( 19 | testConfig = Config{ 20 | Global: nil, 21 | Route: &Route{ 22 | Receiver: "base", 23 | Routes: []*Route{ 24 | {Receiver: "testReceiver"}, 25 | {Receiver: "testReceiver2"}, 26 | { 27 | Receiver: "testReceiver3", 28 | Routes: []*Route{ 29 | {Receiver: "testReceiver"}, 30 | {Receiver: "testReceiverChild1"}, 31 | }, 32 | }, 33 | }, 34 | }, 35 | Receivers: []*Receiver{ 36 | {Name: "base"}, 37 | {Name: "testReceiver"}, 38 | {Name: "testReceiver2"}, 39 | {Name: "testReceiver3"}, 40 | {Name: "testReceiverChild1"}, 41 | }, 42 | } 43 | ) 44 | 45 | func TestConfig_RemoveReceiverFromRoute(t *testing.T) { 46 | copy := deepCopy(testConfig) 47 | copy.RemoveReceiverFromRoute("testReceiver") 48 | assert.Len(t, copy.Route.Routes, 2) 49 | assert.Equal(t, copy.Route.Routes[0].Receiver, "testReceiver2") 50 | assert.Equal(t, copy.Route.Routes[1].Receiver, "testReceiver3") 51 | 52 | assert.Len(t, copy.Route.Routes[1].Routes, 1) 53 | assert.Equal(t, copy.Route.Routes[1].Routes[0].Receiver, "testReceiverChild1") 54 | } 55 | 56 | func TestConfig_SearchRoutesForReceiver(t *testing.T) { 57 | assert.True(t, testConfig.SearchRoutesForReceiver("base")) 58 | assert.True(t, testConfig.SearchRoutesForReceiver("testReceiver2")) 59 | assert.True(t, testConfig.SearchRoutesForReceiver("testReceiver3")) 60 | assert.True(t, testConfig.SearchRoutesForReceiver("testReceiverChild1")) 61 | assert.False(t, testConfig.SearchRoutesForReceiver("foo")) 62 | } 63 | 64 | func TestConfig_InitializeBaseRoute(t *testing.T) { 65 | newRoute := &Route{ 66 | Receiver: "test", 67 | Match: map[string]string{"tenant": "test"}, 68 | } 69 | copy := deepCopy(testConfig) 70 | err := copy.InitializeNetworkBaseRoute(newRoute, "testMatcher", "tenant1") 71 | assert.True(t, copy.SearchRoutesForReceiver("tenant1_tenant_base_route")) 72 | assert.Equal(t, copy.Route.Routes[3].Receiver, "tenant1_tenant_base_route") 73 | assert.Equal(t, copy.Route.Routes[3].Match["testMatcher"], "tenant1") 74 | assert.NoError(t, err) 75 | 76 | err = copy.InitializeNetworkBaseRoute(newRoute, "testMatcher", "tenant1") 77 | assert.EqualError(t, err, "Base route for tenant tenant1 already exists") 78 | } 79 | 80 | func deepCopy(conf Config) (new Config) { 81 | b, _ := json.Marshal(conf) 82 | err := json.Unmarshal(b, &new) 83 | if err != nil { 84 | panic(fmt.Errorf("this shouldn't happen: %v", err)) 85 | } 86 | return new 87 | } 88 | -------------------------------------------------------------------------------- /alertmanager/config/receiver.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package config 9 | 10 | import ( 11 | "strings" 12 | 13 | "github.com/facebookincubator/prometheus-configmanager/alertmanager/common" 14 | 15 | "github.com/prometheus/alertmanager/config" 16 | 17 | "github.com/prometheus/common/model" 18 | ) 19 | 20 | // Receiver uses custom notifier configs to allow for marshaling of secrets. 21 | type Receiver struct { 22 | Name string `yaml:"name" json:"name"` 23 | 24 | SlackConfigs []*SlackConfig `yaml:"slack_configs,omitempty" json:"slack_configs,omitempty"` 25 | WebhookConfigs []*WebhookConfig `yaml:"webhook_configs,omitempty" json:"webhook_configs,omitempty"` 26 | EmailConfigs []*EmailConfig `yaml:"email_configs,omitempty" json:"email_configs,omitempty"` 27 | PagerDutyConfigs []*PagerDutyConfig `yaml:"pagerduty_configs,omitempty" json:"pagerduty_configs,omitempty"` 28 | PushoverConfigs []*PushoverConfig `yaml:"pushover_configs,omitempty" json:"pushover_configs,omitempty"` 29 | } 30 | 31 | // ReceiverJSONWrapper uses custom (JSON compatible) notifier configs to allow 32 | // for marshaling of secrets. 33 | type ReceiverJSONWrapper struct { 34 | Name string `yaml:"name" json:"name"` 35 | 36 | SlackConfigs []*SlackConfig `yaml:"slack_configs,omitempty" json:"slack_configs,omitempty"` 37 | WebhookConfigs []*WebhookConfig `yaml:"webhook_configs,omitempty" json:"webhook_configs,omitempty"` 38 | EmailConfigs []*EmailConfig `yaml:"email_configs,omitempty" json:"email_configs,omitempty"` 39 | PagerDutyConfigs []*PagerDutyConfig `yaml:"pagerduty_configs,omitempty" json:"pagerduty_configs,omitempty"` 40 | PushoverConfigs []*PushoverJSONWrapper `yaml:"pushover_configs,omitempty" json:"pushover_configs,omitempty"` 41 | } 42 | 43 | // Secure replaces the receiver's name with a tenantID prefix 44 | func (r *Receiver) Secure(tenantID string) { 45 | r.Name = SecureReceiverName(r.Name, tenantID) 46 | } 47 | 48 | // Unsecure removes the tenantID prefix from the receiver name 49 | func (r *Receiver) Unsecure(tenantID string) { 50 | r.Name = UnsecureReceiverName(r.Name, tenantID) 51 | } 52 | 53 | func SecureReceiverName(name, tenantID string) string { 54 | return ReceiverTenantPrefix(tenantID) + name 55 | } 56 | 57 | func UnsecureReceiverName(name, tenantID string) string { 58 | if strings.HasPrefix(name, ReceiverTenantPrefix(tenantID)) { 59 | return name[len(ReceiverTenantPrefix(tenantID)):] 60 | } 61 | return name 62 | } 63 | 64 | // SlackConfig uses string instead of SecretURL for the APIURL field so that it 65 | // is marshaled as is instead of being obscured which is how alertmanager handles 66 | // secrets 67 | type SlackConfig struct { 68 | config.NotifierConfig `yaml:",inline" json:"notifier_config,inline"` 69 | HTTPConfig *common.HTTPConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"` 70 | 71 | APIURL string `yaml:"api_url" json:"api_url"` 72 | Channel string `yaml:"channel" json:"channel"` 73 | Username string `yaml:"username" json:"username"` 74 | Color string `yaml:"color,omitempty" json:"color,omitempty"` 75 | Title string `yaml:"title,omitempty" json:"title,omitempty"` 76 | TitleLink string `yaml:"title_link,omitempty" json:"title_link,omitempty"` 77 | Pretext string `yaml:"pretext,omitempty" json:"pretext,omitempty"` 78 | Text string `yaml:"text,omitempty" json:"text,omitempty"` 79 | Fields []*config.SlackField `yaml:"fields,omitempty" json:"fields,omitempty"` 80 | ShortFields bool `yaml:"short_fields,omitempty" json:"short_fields,omitempty"` 81 | Footer string `yaml:"footer,omitempty" json:"footer,omitempty"` 82 | Fallback string `yaml:"fallback,omitempty" json:"fallback,omitempty"` 83 | CallbackID string `yaml:"callback_id,omitempty" json:"callback_id,omitempty"` 84 | IconEmoji string `yaml:"icon_emoji,omitempty" json:"icon_emoji,omitempty"` 85 | IconURL string `yaml:"icon_url,omitempty" json:"icon_url,omitempty"` 86 | ImageURL string `yaml:"image_url,omitempty" json:"image_url,omitempty"` 87 | ThumbURL string `yaml:"thumb_url,omitempty" json:"thumb_url,omitempty"` 88 | LinkNames bool `yaml:"link_names,omitempty" json:"link_names,omitempty"` 89 | Actions []*config.SlackAction `yaml:"actions,omitempty" json:"actions,omitempty"` 90 | } 91 | 92 | // EmailConfig uses string instead of Secret for the AuthPassword and AuthSecret 93 | // field so that it is marshaled as is instead of being obscured which is how 94 | // alertmanager handles secrets. Otherwise the secrets would be obscured on write 95 | // to the yml file, making it unusable. 96 | type EmailConfig struct { 97 | config.NotifierConfig `yaml:",inline" json:"notifier_config,inline"` 98 | 99 | To string `yaml:"to,omitempty" json:"to,omitempty"` 100 | From string `yaml:"from,omitempty" json:"from,omitempty"` 101 | Hello string `yaml:"hello,omitempty" json:"hello,omitempty"` 102 | Smarthost string `yaml:"smarthost,omitempty" json:"smarthost,omitempty"` 103 | AuthUsername string `yaml:"auth_username,omitempty" json:"auth_username,omitempty"` 104 | AuthPassword string `yaml:"auth_password,omitempty" json:"auth_password,omitempty"` 105 | AuthSecret string `yaml:"auth_secret,omitempty" json:"auth_secret,omitempty"` 106 | AuthIdentity string `yaml:"auth_identity,omitempty" json:"auth_identity,omitempty"` 107 | Headers map[string]string `yaml:"headers,omitempty" json:"headers,omitempty"` 108 | HTML string `yaml:"html,omitempty" json:"html,omitempty"` 109 | Text string `yaml:"text,omitempty" json:"text,omitempty"` 110 | RequireTLS *bool `yaml:"require_tls,omitempty" json:"require_tls,omitempty"` 111 | } 112 | 113 | // PagerDutyConfig uses string instead of Secret for the RoutingKey and ServiceKey 114 | // field so that it is mashaled as is instead of being obscured which is how 115 | // alertmanager handles secrets. Otherwise the secrets would be obscured on 116 | // write to the yml file, making it unusable. 117 | type PagerDutyConfig struct { 118 | config.NotifierConfig `yaml:",inline" json:"notifier_config,inline"` 119 | HTTPConfig *common.HTTPConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"` 120 | 121 | RoutingKey string `yaml:"routing_key,omitempty" json:"routing_key,omitempty"` 122 | ServiceKey string `yaml:"service_key,omitempty" json:"service_key,omitempty"` 123 | URL string `yaml:"url,omitempty" json:"url,omitempty"` 124 | Client string `yaml:"client,omitempty" json:"client,omitempty"` 125 | ClientURL string `yaml:"client_url,omitempty" json:"client_url,omitempty"` 126 | Description string `yaml:"description,omitempty" json:"description,omitempty"` 127 | Severity string `yaml:"severity,omitempty" json:"severity,omitempty"` 128 | Details map[string]string `yaml:"details,omitempty" json:"details,omitempty"` 129 | Images []*config.PagerdutyImage `yaml:"images,omitempty" json:"images,omitempty"` 130 | Links []*config.PagerdutyLink `yaml:"links,omitempty" json:"links,omitempty"` 131 | } 132 | 133 | // PushoverConfig uses string instead of Secret for the UserKey and Token 134 | // field so that it is mashaled as is instead of being obscured which is how 135 | // alertmanager handles secrets. Otherwise the secrets would be obscured on 136 | // write to the yml file, making it unusable. 137 | type PushoverConfig struct { 138 | config.NotifierConfig `yaml:",inline" json:"notifier_config,inline"` 139 | HTTPConfig *common.HTTPConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"` 140 | 141 | UserKey string `yaml:"user_key" json:"user_key"` 142 | Token string `yaml:"token" json:"token"` 143 | Title string `yaml:"title,omitempty" json:"title,omitempty"` 144 | Message string `yaml:"message,omitempty" json:"message,omitempty"` 145 | URL string `yaml:"url,omitempty" json:"url,omitempty"` 146 | Priority string `yaml:"priority,omitempty" json:"priority,omitempty"` 147 | Retry model.Duration `yaml:"retry,omitempty" json:"retry,omitempty"` 148 | Expire model.Duration `yaml:"expire,omitempty" json:"expire,omitempty"` 149 | } 150 | 151 | type PushoverJSONWrapper struct { 152 | config.NotifierConfig `yaml:",inline" json:"notifier_config,inline"` 153 | HTTPConfig *common.HTTPConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"` 154 | 155 | UserKey string `yaml:"user_key" json:"user_key"` 156 | Token string `yaml:"token" json:"token"` 157 | Title string `yaml:"title,omitempty" json:"title,omitempty"` 158 | Message string `yaml:"message,omitempty" json:"message,omitempty"` 159 | URL string `yaml:"url,omitempty" json:"url,omitempty"` 160 | Priority string `yaml:"priority,omitempty" json:"priority,omitempty"` 161 | Retry string `yaml:"retry,omitempty" json:"retry,omitempty"` 162 | Expire string `yaml:"expire,omitempty" json:"expire,omitempty"` 163 | } 164 | 165 | // ToReceiverFmt convers the JSONWrapper object to a true Receiver object. This will 166 | // only be necessary when dealing with Pushover objects for the time being (due to 167 | // complexities surrounding JSON unmarshalling) 168 | func (r *ReceiverJSONWrapper) ToReceiverFmt() (Receiver, error) { 169 | receiver := Receiver{ 170 | Name: r.Name, 171 | SlackConfigs: r.SlackConfigs, 172 | WebhookConfigs: r.WebhookConfigs, 173 | EmailConfigs: r.EmailConfigs, 174 | PagerDutyConfigs: r.PagerDutyConfigs, 175 | } 176 | 177 | for _, p := range r.PushoverConfigs { 178 | pushoverConf := PushoverConfig{ 179 | NotifierConfig: p.NotifierConfig, 180 | HTTPConfig: p.HTTPConfig, 181 | UserKey: p.UserKey, 182 | Token: p.Token, 183 | Title: p.Title, 184 | Message: p.Message, 185 | URL: p.URL, 186 | Priority: p.Priority, 187 | } 188 | if p.Retry != "" { 189 | modelRetry, err := model.ParseDuration(p.Retry) 190 | if err != nil { 191 | return receiver, err 192 | } 193 | pushoverConf.Retry = modelRetry 194 | } 195 | if p.Expire != "" { 196 | modelExpire, err := model.ParseDuration(p.Expire) 197 | if err != nil { 198 | return receiver, err 199 | } 200 | pushoverConf.Expire = modelExpire 201 | } 202 | receiver.PushoverConfigs = append(receiver.PushoverConfigs, &pushoverConf) 203 | } 204 | 205 | return receiver, nil 206 | } 207 | 208 | // WebhookConfig is a copy of prometheus/alertmanager/config.WebhookConfig with 209 | // alertmanager-configurer's custom HTTPConfig 210 | type WebhookConfig struct { 211 | config.NotifierConfig `yaml:",inline" json:"notifier_config,inline"` 212 | 213 | HTTPConfig *common.HTTPConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"` 214 | 215 | URL *config.URL `yaml:"url" json:"url"` 216 | } 217 | 218 | func ReceiverTenantPrefix(tenantID string) string { 219 | return strings.Replace(tenantID, "_", "", -1) + "_" 220 | } 221 | -------------------------------------------------------------------------------- /alertmanager/config/receiver_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package config_test 9 | 10 | import ( 11 | "strings" 12 | "testing" 13 | 14 | "github.com/facebookincubator/prometheus-configmanager/alertmanager/config" 15 | tc "github.com/facebookincubator/prometheus-configmanager/alertmanager/testcommon" 16 | 17 | amconfig "github.com/prometheus/alertmanager/config" 18 | "github.com/stretchr/testify/assert" 19 | "gopkg.in/yaml.v2" 20 | ) 21 | 22 | const testNID = "test" 23 | 24 | func TestConfig_Validate(t *testing.T) { 25 | defaultGlobalConf := config.DefaultGlobalConfig() 26 | validConfig := config.Config{ 27 | Route: &tc.SampleRoute, 28 | Receivers: []*config.Receiver{&tc.SampleReceiver, &tc.SampleSlackReceiver}, 29 | Global: &defaultGlobalConf, 30 | } 31 | err := validConfig.Validate() 32 | assert.NoError(t, err) 33 | 34 | invalidConfig := config.Config{ 35 | Route: &tc.SampleRoute, 36 | Receivers: []*config.Receiver{}, 37 | Global: &defaultGlobalConf, 38 | } 39 | err = invalidConfig.Validate() 40 | assert.EqualError(t, err, `undefined receiver "testReceiver" used in route`) 41 | 42 | invalidPushoverReceiverJSON := config.PushoverJSONWrapper{ 43 | UserKey: "0", 44 | Token: "0", 45 | Expire: "1s1m", 46 | } 47 | invalidPushoverReceiverWrapper := config.ReceiverJSONWrapper{ 48 | Name: "invalidPushover", 49 | PushoverConfigs: []*config.PushoverJSONWrapper{&invalidPushoverReceiverJSON}, 50 | } 51 | 52 | _, err = invalidPushoverReceiverWrapper.ToReceiverFmt() 53 | assert.EqualError(t, err, `not a valid duration string: "1s1m"`) 54 | 55 | validPushoverReceiverJSON := config.PushoverJSONWrapper{ 56 | UserKey: "0", 57 | Token: "0", 58 | Expire: "1m", 59 | } 60 | validPushoverWrapper := config.ReceiverJSONWrapper{ 61 | Name: "validPushover", 62 | PushoverConfigs: []*config.PushoverJSONWrapper{&validPushoverReceiverJSON}, 63 | } 64 | validPushoverReceiver, err := validPushoverWrapper.ToReceiverFmt() 65 | assert.NoError(t, err) 66 | 67 | validPushoverConfig := config.Config{ 68 | Route: &config.Route{ 69 | Receiver: "validPushover", 70 | }, 71 | Receivers: []*config.Receiver{&validPushoverReceiver}, 72 | Global: &defaultGlobalConf, 73 | } 74 | err = validPushoverConfig.Validate() 75 | assert.NoError(t, err) 76 | 77 | invalidSlackReceiver := config.Receiver{ 78 | Name: "invalidSlack", 79 | SlackConfigs: []*config.SlackConfig{ 80 | { 81 | APIURL: "invalidURL", 82 | }, 83 | }, 84 | } 85 | 86 | invalidSlackConfig := config.Config{ 87 | Route: &config.Route{ 88 | Receiver: "invalidSlack", 89 | }, 90 | Receivers: []*config.Receiver{&invalidSlackReceiver}, 91 | Global: &defaultGlobalConf, 92 | } 93 | err = invalidSlackConfig.Validate() 94 | assert.EqualError(t, err, `unsupported scheme "" for URL`) 95 | 96 | // Fail if action is missing a type 97 | invalidSlackAction := config.Config{ 98 | Route: &config.Route{ 99 | Receiver: "invalidSlackAction", 100 | }, 101 | Receivers: []*config.Receiver{{ 102 | Name: "invalidSlackAction", 103 | SlackConfigs: []*config.SlackConfig{{ 104 | APIURL: "http://slack.com", 105 | Actions: []*amconfig.SlackAction{{ 106 | URL: "test.com", 107 | Text: "test", 108 | }}, 109 | }}, 110 | }}, 111 | } 112 | err = invalidSlackAction.Validate() 113 | assert.EqualError(t, err, `missing type in Slack action configuration`) 114 | 115 | // Fail if pager duty contains no ServiceKey or RoutingKey 116 | invalidPagerDuty := config.Config{ 117 | Route: &config.Route{ 118 | Receiver: "invalidPagerDuty", 119 | }, 120 | Receivers: []*config.Receiver{{ 121 | Name: "invalidPagerDuty", 122 | PagerDutyConfigs: []*config.PagerDutyConfig{{ 123 | Links: []*amconfig.PagerdutyLink{{ 124 | Text: "test", 125 | }}, 126 | }}, 127 | }}, 128 | } 129 | err = invalidPagerDuty.Validate() 130 | assert.EqualError(t, err, `missing service or routing key in PagerDuty config`) 131 | } 132 | 133 | func TestConfig_GetReceiver(t *testing.T) { 134 | rec := tc.SampleConfig.GetReceiver("testReceiver") 135 | assert.NotNil(t, rec) 136 | 137 | rec = tc.SampleConfig.GetReceiver("slack_receiver") 138 | assert.NotNil(t, rec) 139 | 140 | rec = tc.SampleConfig.GetReceiver("webhook_receiver") 141 | assert.NotNil(t, rec) 142 | 143 | rec = tc.SampleConfig.GetReceiver("email_receiver") 144 | assert.NotNil(t, rec) 145 | 146 | rec = tc.SampleConfig.GetReceiver("pagerduty_receiver") 147 | assert.NotNil(t, rec) 148 | 149 | rec = tc.SampleConfig.GetReceiver("pushover_receiver") 150 | assert.NotNil(t, rec) 151 | 152 | rec = tc.SampleConfig.GetReceiver("nonRoute") 153 | assert.Nil(t, rec) 154 | } 155 | 156 | func TestConfig_GetRouteIdx(t *testing.T) { 157 | idx := tc.SampleConfig.GetRouteIdx("testReceiver") 158 | assert.Equal(t, 0, idx) 159 | 160 | idx = tc.SampleConfig.GetRouteIdx("slack_receiver") 161 | assert.Equal(t, 1, idx) 162 | 163 | idx = tc.SampleConfig.GetRouteIdx("nonRoute") 164 | assert.Equal(t, -1, idx) 165 | } 166 | 167 | func TestReceiver_Secure(t *testing.T) { 168 | rec := config.Receiver{Name: "receiverName"} 169 | rec.Secure(testNID) 170 | assert.Equal(t, "test_receiverName", rec.Name) 171 | } 172 | 173 | func TestReceiver_Unsecure(t *testing.T) { 174 | rec := config.Receiver{Name: "receiverName"} 175 | rec.Secure(testNID) 176 | assert.Equal(t, "test_receiverName", rec.Name) 177 | 178 | rec.Unsecure(testNID) 179 | assert.Equal(t, "receiverName", rec.Name) 180 | } 181 | 182 | func TestMarshalYamlEmailConfig(t *testing.T) { 183 | valTrue := true 184 | emailConf := config.EmailConfig{ 185 | To: "test@mail.com", 186 | RequireTLS: &valTrue, 187 | Headers: map[string]string{"test": "true", "new": "old"}, 188 | } 189 | ymlData, err := yaml.Marshal(emailConf) 190 | assert.NoError(t, err) 191 | assert.True(t, strings.Contains(string(ymlData), "require_tls: true")) 192 | } 193 | -------------------------------------------------------------------------------- /alertmanager/config/route.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package config 9 | 10 | import ( 11 | "github.com/prometheus/alertmanager/config" 12 | "github.com/prometheus/common/model" 13 | ) 14 | 15 | // Route provides a struct to marshal/unmarshal into an alertmanager route 16 | // since that struct does not support json encoding 17 | type Route struct { 18 | Receiver string `yaml:"receiver,omitempty" json:"receiver,omitempty"` 19 | 20 | GroupByStr []string `yaml:"group_by,omitempty" json:"group_by,omitempty"` 21 | GroupBy []model.LabelName `yaml:"-" json:"-"` 22 | GroupByAll bool `yaml:"-" json:"-"` 23 | 24 | Match map[string]string `yaml:"match,omitempty" json:"match,omitempty"` 25 | MatchRE map[string]config.Regexp `yaml:"match_re,omitempty" json:"match_re,omitempty"` 26 | Continue bool `yaml:"continue,omitempty" json:"continue,omitempty"` 27 | Routes []*Route `yaml:"routes,omitempty" json:"routes,omitempty"` 28 | 29 | GroupWait string `yaml:"group_wait,omitempty" json:"group_wait,omitempty"` 30 | GroupInterval string `yaml:"group_interval,omitempty" json:"group_interval,omitempty"` 31 | RepeatInterval string `yaml:"repeat_interval,omitempty" json:"repeat_interval,omitempty"` 32 | } 33 | -------------------------------------------------------------------------------- /alertmanager/docs/swagger.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # This source code is licensed under the MIT license found in the 3 | # LICENSE file in the root directory of this source tree. 4 | 5 | --- 6 | swagger: '2.0' 7 | info: 8 | title: Alertmanager Configurer Model Definitions and Paths 9 | description: Alertmanager Configurer REST APIs 10 | version: 0.1.0 11 | 12 | paths: 13 | /{tenant_id}/alert_receiver: 14 | post: 15 | summary: Create new alert receiver 16 | tags: 17 | - Receivers 18 | parameters: 19 | - $ref: '#/parameters/tenant_id' 20 | - in: body 21 | name: receiver_config 22 | description: Alert receiver that is to be added 23 | required: true 24 | schema: 25 | $ref: '#/definitions/receiver_config' 26 | responses: 27 | '201': 28 | description: Created 29 | default: 30 | $ref: '#/responses/UnexpectedError' 31 | get: 32 | summary: Retrive alert receivers 33 | tags: 34 | - Receivers 35 | parameters: 36 | - $ref: '#/parameters/tenant_id' 37 | responses: 38 | '200': 39 | description: List of alert receivers 40 | schema: 41 | type: array 42 | items: 43 | $ref: '#/definitions/receiver_config' 44 | default: 45 | $ref: '#/responses/UnexpectedError' 46 | delete: 47 | summary: Delete alert receiver 48 | tags: 49 | - Receivers 50 | parameters: 51 | - $ref: '#/parameters/tenant_id' 52 | - in: query 53 | name: receiver 54 | description: Receiver name to be deleted 55 | required: true 56 | type: string 57 | responses: 58 | '200': 59 | description: Deleted 60 | default: 61 | $ref: '#/responses/UnexpectedError' 62 | 63 | /{tenant_id}/receiver/{receiver_name}: 64 | put: 65 | summary: Update existing alert receiver 66 | tags: 67 | - Receivers 68 | parameters: 69 | - $ref: '#/parameters/tenant_id' 70 | - in: path 71 | name: receiver_name 72 | description: Name of receiver to be updated 73 | required: true 74 | type: string 75 | - in: body 76 | name: receiver_config 77 | description: Updated alert receiver 78 | required: true 79 | schema: 80 | $ref: '#/definitions/receiver_config' 81 | responses: 82 | '200': 83 | description: Updated 84 | default: 85 | $ref: '#/responses/UnexpectedError' 86 | 87 | /{tenant_id}/receiver/route: 88 | get: 89 | summary: Retrieve alert routing tree 90 | tags: 91 | - Routes 92 | parameters: 93 | - $ref: '#/parameters/tenant_id' 94 | responses: 95 | '200': 96 | description: Alerting tree 97 | schema: 98 | $ref: '#/definitions/routing_tree' 99 | post: 100 | summary: Modify alert routing tree 101 | tags: 102 | - Routes 103 | parameters: 104 | - $ref: '#/parameters/tenant_id' 105 | - in: body 106 | name: route 107 | description: Alert routing tree to be used 108 | required: true 109 | schema: 110 | $ref: '#/definitions/routing_tree' 111 | responses: 112 | '200': 113 | description: OK 114 | default: 115 | $ref: '#/responses/UnexpectedError' 116 | 117 | parameters: 118 | tenant_id: 119 | description: Tenant ID 120 | in: path 121 | name: tenant_id 122 | required: true 123 | type: string 124 | 125 | definitions: 126 | receiver_config: 127 | type: object 128 | required: 129 | - name 130 | properties: 131 | name: 132 | type: string 133 | slack_configs: 134 | type: array 135 | items: 136 | $ref: '#/definitions/slack_receiver' 137 | 138 | slack_receiver: 139 | type: object 140 | required: 141 | - api_url 142 | properties: 143 | api_url: 144 | type: string 145 | channel: 146 | type: string 147 | username: 148 | type: string 149 | color: 150 | type: string 151 | title: 152 | type: string 153 | pretext: 154 | type: string 155 | text: 156 | type: string 157 | fields: 158 | type: array 159 | items: 160 | $ref: '#/definitions/slack_field' 161 | short_fields: 162 | type: boolean 163 | footer: 164 | type: string 165 | fallback: 166 | type: string 167 | callback_id: 168 | type: string 169 | icon_emoji: 170 | type: string 171 | icon_url: 172 | type: string 173 | image_url: 174 | type: string 175 | thumb_url: 176 | type: string 177 | link_names: 178 | type: boolean 179 | actions: 180 | type: array 181 | items: 182 | $ref: '#/definitions/slack_action' 183 | 184 | slack_field: 185 | type: object 186 | required: 187 | - title 188 | - value 189 | properties: 190 | title: 191 | type: string 192 | value: 193 | type: string 194 | short: 195 | type: boolean 196 | 197 | slack_action: 198 | type: object 199 | required: 200 | - type 201 | - text 202 | - url 203 | properties: 204 | type: 205 | type: string 206 | text: 207 | type: string 208 | url: 209 | type: string 210 | style: 211 | type: string 212 | name: 213 | type: string 214 | value: 215 | type: string 216 | confirm: 217 | $ref: '#/definitions/slack_confirm_field' 218 | 219 | slack_confirm_field: 220 | type: object 221 | required: 222 | - text 223 | - title 224 | - ok_text 225 | - dismiss_text 226 | properties: 227 | text: 228 | type: string 229 | title: 230 | type: string 231 | ok_text: 232 | type: string 233 | dismiss_text: 234 | type: string 235 | 236 | routing_tree: 237 | type: object 238 | required: 239 | - receiver 240 | properties: 241 | receiver: 242 | type: string 243 | group_by: 244 | type: array 245 | items: 246 | type: string 247 | match: 248 | type: object 249 | properties: 250 | label: 251 | type: string 252 | value: 253 | type: string 254 | match_re: 255 | type: object 256 | properties: 257 | label: 258 | type: string 259 | value: 260 | type: string 261 | continue: 262 | type: boolean 263 | routes: 264 | type: array 265 | items: 266 | $ref: '#/definitions/routing_tree' 267 | group_wait: 268 | type: string 269 | group_interval: 270 | type: string 271 | repeat_interval: 272 | type: string 273 | 274 | error: 275 | type: object 276 | required: 277 | - message 278 | properties: 279 | message: 280 | example: Error string 281 | type: string 282 | 283 | responses: 284 | UnexpectedError: 285 | description: Unexpected Error 286 | schema: 287 | $ref: '#/definitions/error' 288 | -------------------------------------------------------------------------------- /alertmanager/handlers/template_handlers.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package handlers 9 | 10 | import ( 11 | "fmt" 12 | "io/ioutil" 13 | "net/http" 14 | 15 | "github.com/facebookincubator/prometheus-configmanager/alertmanager/client" 16 | "github.com/labstack/echo" 17 | ) 18 | 19 | func GetGetTemplateFileHandler(amClient client.AlertmanagerClient, tmplClient client.TemplateClient) func(c echo.Context) error { 20 | return func(c echo.Context) error { 21 | filename := c.Get(templateFilenameParam).(string) 22 | exists, err := fileExists(amClient, tmplClient, filename) 23 | if err != nil { 24 | return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) 25 | } 26 | if !exists { 27 | return echo.NewHTTPError(http.StatusBadRequest, fmt.Errorf("error getting file %s: file does not exist", filename).Error()) 28 | } 29 | 30 | file, err := tmplClient.GetTemplateFile(filename) 31 | if err != nil { 32 | return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("error getting template file: %v", err)) 33 | } 34 | return c.JSON(http.StatusOK, file) 35 | } 36 | } 37 | 38 | func GetPostTemplateFileHandler(amClient client.AlertmanagerClient, tmplClient client.TemplateClient) func(c echo.Context) error { 39 | return func(c echo.Context) error { 40 | filename := c.Get(templateFilenameParam).(string) 41 | 42 | exists, err := fileExists(amClient, tmplClient, filename) 43 | if err != nil { 44 | return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) 45 | } 46 | if exists { 47 | return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("file %s already exists", filename)) 48 | } 49 | 50 | body, err := readStringBody(c) 51 | if err != nil { 52 | return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) 53 | } 54 | 55 | err = tmplClient.CreateTemplateFile(filename, body) 56 | if err != nil { 57 | return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("error creating template file: %v", err)) 58 | } 59 | 60 | err = amClient.AddTemplateFile(getFullFilePath(filename, tmplClient)) 61 | if err != nil { 62 | return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("error creating template file: %v", err)) 63 | } 64 | 65 | return c.String(http.StatusOK, "Created") 66 | } 67 | } 68 | 69 | func GetPutTemplateFileHandler(amClient client.AlertmanagerClient, tmplClient client.TemplateClient) func(c echo.Context) error { 70 | return func(c echo.Context) error { 71 | filename := c.Get(templateFilenameParam).(string) 72 | 73 | exists, err := fileExists(amClient, tmplClient, filename) 74 | if err != nil { 75 | return echo.NewHTTPError(http.StatusInternalServerError, err) 76 | } 77 | if !exists { 78 | return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("error editing file %s: file does not exist", filename)) 79 | } 80 | 81 | body, err := readStringBody(c) 82 | if err != nil { 83 | return echo.NewHTTPError(http.StatusInternalServerError, err) 84 | } 85 | 86 | err = tmplClient.EditTemplateFile(filename, body) 87 | if err != nil { 88 | return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("error editing template file: %v", err)) 89 | } 90 | return c.NoContent(http.StatusOK) 91 | } 92 | } 93 | 94 | func GetDeleteTemplateFileHandler(amClient client.AlertmanagerClient, tmplClient client.TemplateClient) func(c echo.Context) error { 95 | return func(c echo.Context) error { 96 | filename := c.Get(templateFilenameParam).(string) 97 | 98 | exists, err := fileExists(amClient, tmplClient, filename) 99 | if err != nil { 100 | return echo.NewHTTPError(http.StatusInternalServerError, err) 101 | } 102 | if !exists { 103 | return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("error deleting file: file %s does not exist", filename)) 104 | } 105 | 106 | err = tmplClient.DeleteTemplateFile(filename) 107 | if err != nil { 108 | return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("error deleting template file: %v", err)) 109 | } 110 | 111 | err = amClient.RemoveTemplateFile(getFullFilePath(filename, tmplClient)) 112 | if err != nil { 113 | return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("error deleting template file: %v", err)) 114 | } 115 | 116 | return c.NoContent(http.StatusOK) 117 | } 118 | } 119 | 120 | func GetGetTemplatesHandler(amClient client.AlertmanagerClient, tmplClient client.TemplateClient) func(c echo.Context) error { 121 | return func(c echo.Context) error { 122 | filename := c.Get(templateFilenameParam).(string) 123 | 124 | exists, err := fileExists(amClient, tmplClient, filename) 125 | if err != nil { 126 | return echo.NewHTTPError(http.StatusInternalServerError, err) 127 | } 128 | if !exists { 129 | return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("error getting file: file %s does not exist", filename)) 130 | } 131 | 132 | tmps, err := tmplClient.GetTemplates(filename) 133 | if err != nil { 134 | return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("error getting templates: %s", err.Error())) 135 | } 136 | return c.JSON(http.StatusOK, tmps) 137 | } 138 | } 139 | 140 | func GetGetTemplateHandler(amClient client.AlertmanagerClient, tmplClient client.TemplateClient) func(c echo.Context) error { 141 | return func(c echo.Context) error { 142 | filename := c.Get(templateFilenameParam).(string) 143 | tmplName := c.Get(templateNameParam).(string) 144 | 145 | exists, err := fileExists(amClient, tmplClient, filename) 146 | if err != nil { 147 | return echo.NewHTTPError(http.StatusInternalServerError, err) 148 | } 149 | if !exists { 150 | return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("error getting template: file %s does not exist", filename)) 151 | } 152 | 153 | tmpl, err := tmplClient.GetTemplate(filename, tmplName) 154 | if err != nil { 155 | return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("error getting template: %s", err.Error())) 156 | } 157 | return c.JSON(http.StatusOK, tmpl) 158 | } 159 | } 160 | 161 | func GetPostTemplateHandler(amClient client.AlertmanagerClient, tmplClient client.TemplateClient) func(c echo.Context) error { 162 | return func(c echo.Context) error { 163 | filename := c.Get(templateFilenameParam).(string) 164 | tmplName := c.Get(templateNameParam).(string) 165 | 166 | tmplText, err := readStringBody(c) 167 | if err != nil { 168 | return echo.NewHTTPError(http.StatusBadRequest, err.Error()) 169 | } 170 | 171 | exists, err := fileExists(amClient, tmplClient, filename) 172 | if err != nil { 173 | return echo.NewHTTPError(http.StatusInternalServerError, err) 174 | } 175 | if !exists { 176 | return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("error getting file: file %s does not exist", filename)) 177 | } 178 | 179 | err = tmplClient.AddTemplate(filename, tmplName, tmplText) 180 | if err != nil { 181 | return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("error adding template: %s", err.Error())) 182 | } 183 | return c.NoContent(http.StatusOK) 184 | } 185 | } 186 | 187 | func GetPutTemplateHandler(amClient client.AlertmanagerClient, tmplClient client.TemplateClient) func(c echo.Context) error { 188 | return func(c echo.Context) error { 189 | filename := c.Get(templateFilenameParam).(string) 190 | tmplName := c.Get(templateNameParam).(string) 191 | 192 | tmplText, err := readStringBody(c) 193 | if err != nil { 194 | return echo.NewHTTPError(http.StatusBadRequest, err.Error()) 195 | } 196 | 197 | exists, err := fileExists(amClient, tmplClient, filename) 198 | if err != nil { 199 | return echo.NewHTTPError(http.StatusInternalServerError, err) 200 | } 201 | if !exists { 202 | return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("error getting template: file %s does not exist", filename)) 203 | } 204 | 205 | err = tmplClient.EditTemplate(filename, tmplName, tmplText) 206 | if err != nil { 207 | return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("error editing template: %s", err.Error())) 208 | } 209 | return c.NoContent(http.StatusOK) 210 | } 211 | } 212 | 213 | func GetDeleteTemplateHandler(amClient client.AlertmanagerClient, tmplClient client.TemplateClient) func(c echo.Context) error { 214 | return func(c echo.Context) error { 215 | filename := c.Get(templateFilenameParam).(string) 216 | tmplName := c.Get(templateNameParam).(string) 217 | 218 | exists, err := fileExists(amClient, tmplClient, filename) 219 | if err != nil { 220 | return echo.NewHTTPError(http.StatusInternalServerError, err) 221 | } 222 | if !exists { 223 | return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("error getting template: file %s does not exist", filename)) 224 | } 225 | 226 | err = tmplClient.DeleteTemplate(filename, tmplName) 227 | if err != nil { 228 | return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("error deleting template: %s", err.Error())) 229 | } 230 | return c.NoContent(http.StatusOK) 231 | } 232 | } 233 | 234 | func stringParamProvider(paramName string) echo.MiddlewareFunc { 235 | return func(next echo.HandlerFunc) echo.HandlerFunc { 236 | return func(c echo.Context) error { 237 | requestedParam := c.Param(paramName) 238 | if requestedParam == "" { 239 | return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Must provide %s parameter", paramName)) 240 | } 241 | c.Set(paramName, requestedParam) 242 | return next(c) 243 | } 244 | } 245 | } 246 | 247 | func fileExists(amClient client.AlertmanagerClient, tmplClient client.TemplateClient, filename string) (bool, error) { 248 | files, err := amClient.GetTemplateFileList() 249 | if err != nil { 250 | return false, err 251 | } 252 | for _, file := range files { 253 | if file == getFullFilePath(filename, tmplClient) { 254 | return true, nil 255 | } 256 | } 257 | return false, nil 258 | } 259 | 260 | func getFullFilePath(filename string, tmplClient client.TemplateClient) string { 261 | return tmplClient.Root() + filename + client.TemplateFilePostfix 262 | } 263 | 264 | func readStringBody(c echo.Context) (string, error) { 265 | body, err := ioutil.ReadAll(c.Request().Body) 266 | if err != nil { 267 | return string(body), fmt.Errorf("error reading request body: %v", err) 268 | } 269 | return string(body), nil 270 | } 271 | -------------------------------------------------------------------------------- /alertmanager/migration/migration.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package main 9 | 10 | import ( 11 | "flag" 12 | "fmt" 13 | "regexp" 14 | "strings" 15 | 16 | "github.com/facebookincubator/prometheus-configmanager/alertmanager/config" 17 | "github.com/facebookincubator/prometheus-configmanager/fsclient" 18 | 19 | "github.com/golang/glog" 20 | "gopkg.in/yaml.v2" 21 | ) 22 | 23 | const ( 24 | defaultAlertmanagerConfigPath = "./alertmanager.yml" 25 | ) 26 | 27 | func main() { 28 | alertmanagerConfPath := flag.String("alertmanager-conf", defaultAlertmanagerConfigPath, fmt.Sprintf("Path to alertmanager configuration file. Default is %s", defaultAlertmanagerConfigPath)) 29 | flag.Parse() 30 | 31 | fsClient := fsclient.NewFSClient("/") 32 | 33 | // Read config file 34 | configFile := config.Config{} 35 | file, err := fsClient.ReadFile(*alertmanagerConfPath) 36 | if err != nil { 37 | glog.Fatalf("error reading config files: %v", err) 38 | } 39 | err = yaml.Unmarshal(file, &configFile) 40 | if err != nil { 41 | glog.Fatalf("error marshaling config file: %v", err) 42 | } 43 | 44 | // Do tenancy migration 45 | migrateToTenantBasedConfig(&configFile) 46 | 47 | // Write config file 48 | yamlFile, err := yaml.Marshal(configFile) 49 | if err != nil { 50 | glog.Fatalf("error marshaling config file: %v", err) 51 | } 52 | err = fsClient.WriteFile(*alertmanagerConfPath, yamlFile, 0660) 53 | if err != nil { 54 | glog.Fatalf("error writing config file: %v", err) 55 | } 56 | 57 | glog.Infof("Migrations completed successfully") 58 | } 59 | 60 | // This is necessary due to the change from 'network' based tenancy to 'tenant' 61 | // based tenancy. Replaces 'network_base_route' with 'tenant_base_route' 62 | const deprecatedTenancyPostfix = "network_base_route" 63 | 64 | func migrateToTenantBasedConfig(conf *config.Config) { 65 | for _, route := range conf.Route.Routes { 66 | matched, _ := regexp.MatchString(fmt.Sprintf(".*_%s", deprecatedTenancyPostfix), route.Receiver) 67 | if matched { 68 | migratedName := strings.Replace(route.Receiver, deprecatedTenancyPostfix, config.TenantBaseRoutePostfix, 1) 69 | route.Receiver = migratedName 70 | } 71 | } 72 | for _, receiver := range conf.Receivers { 73 | matched, _ := regexp.MatchString(fmt.Sprintf(".*_%s", deprecatedTenancyPostfix), receiver.Name) 74 | if matched { 75 | migratedName := strings.Replace(receiver.Name, deprecatedTenancyPostfix, config.TenantBaseRoutePostfix, 1) 76 | receiver.Name = migratedName 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /alertmanager/server.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package main 9 | 10 | import ( 11 | "flag" 12 | "fmt" 13 | "strings" 14 | 15 | "github.com/facebookincubator/prometheus-configmanager/alertmanager/client" 16 | "github.com/facebookincubator/prometheus-configmanager/alertmanager/handlers" 17 | "github.com/facebookincubator/prometheus-configmanager/fsclient" 18 | "github.com/facebookincubator/prometheus-configmanager/prometheus/alert" 19 | 20 | "github.com/golang/glog" 21 | "github.com/labstack/echo" 22 | "github.com/labstack/echo/middleware" 23 | ) 24 | 25 | const ( 26 | defaultPort = "9101" 27 | defaultAlertmanagerURL = "alertmanager:9093" 28 | defaultAlertmanagerConfigPath = "./alertmanager.yml" 29 | defaultTemplateDir = "./templates/" 30 | ) 31 | 32 | func main() { 33 | port := flag.String("port", defaultPort, fmt.Sprintf("Port to listen for requests. Default is %s", defaultPort)) 34 | alertmanagerConfPath := flag.String("alertmanager-conf", defaultAlertmanagerConfigPath, fmt.Sprintf("Path to alertmanager configuration file. Default is %s", defaultAlertmanagerConfigPath)) 35 | alertmanagerURL := flag.String("alertmanagerURL", defaultAlertmanagerURL, fmt.Sprintf("URL of the alertmanager instance that is being used. Default is %s", defaultAlertmanagerURL)) 36 | matcherLabel := flag.String("multitenant-label", "", "LabelName to use for enabling multitenancy through route matching. Leave empty for single tenant use cases.") 37 | templateDirPath := flag.String("template-directory", defaultTemplateDir, fmt.Sprintf("Directory where template files are stored. Default is %s", defaultTemplateDir)) 38 | deleteRoutesByDefault := flag.Bool("delete-route-with-receiver", false, fmt.Sprintf("When a receiver is deleted, also delete all references in the route tree. Otherwise deleting before modifying tree will throw error.")) 39 | flag.Parse() 40 | 41 | if !strings.HasSuffix(*templateDirPath, "/") { 42 | *templateDirPath += "/" 43 | } 44 | 45 | tenancy := &alert.TenancyConfig{ 46 | RestrictorLabel: *matcherLabel, 47 | } 48 | 49 | e := echo.New() 50 | e.Use(middleware.CORS()) 51 | e.Use(middleware.Logger()) 52 | 53 | fileLocks, err := alert.NewFileLocker(alert.NewDirectoryClient(*templateDirPath)) 54 | if err != nil { 55 | panic(fmt.Errorf("error configuring file configmanager: %v", err)) 56 | } 57 | 58 | config := client.ClientConfig{ 59 | ConfigPath: *alertmanagerConfPath, 60 | AlertmanagerURL: *alertmanagerURL, 61 | FsClient: fsclient.NewFSClient("/"), 62 | Tenancy: tenancy, 63 | DeleteRoutes: *deleteRoutesByDefault, 64 | } 65 | receiverClient := client.NewClient(config) 66 | templateClient := client.NewTemplateClient(fsclient.NewFSClient(*templateDirPath), fileLocks) 67 | 68 | handlers.RegisterBaseHandlers(e) 69 | handlers.RegisterV0Handlers(e, receiverClient) 70 | handlers.RegisterV1Handlers(e, receiverClient, templateClient) 71 | 72 | glog.Infof("Alertmanager Config server listening on port: %s\n", *port) 73 | e.Logger.Fatal(e.Start(fmt.Sprintf(":%s", *port))) 74 | } 75 | -------------------------------------------------------------------------------- /alertmanager/testcommon/configs.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package testcommon 9 | 10 | import ( 11 | "net/url" 12 | 13 | "github.com/facebookincubator/prometheus-configmanager/alertmanager/config" 14 | 15 | amconfig "github.com/prometheus/alertmanager/config" 16 | ) 17 | 18 | var ( 19 | sampleURL, _ = url.Parse("http://test.com") 20 | SampleRoute = config.Route{ 21 | Receiver: "testReceiver", 22 | Routes: []*config.Route{ 23 | { 24 | Receiver: "testReceiver", 25 | }, 26 | { 27 | Receiver: "slack_receiver", 28 | }, 29 | }, 30 | } 31 | SampleReceiver = config.Receiver{ 32 | Name: "testReceiver", 33 | } 34 | SampleSlackReceiver = config.Receiver{ 35 | Name: "slack_receiver", 36 | SlackConfigs: []*config.SlackConfig{{ 37 | APIURL: "http://slack.com/12345", 38 | Username: "slack_user", 39 | Channel: "slack_alert_channel", 40 | }}, 41 | } 42 | SamplePagerDutyReceiver = config.Receiver{ 43 | Name: "pagerduty_receiver", 44 | PagerDutyConfigs: []*config.PagerDutyConfig{{ 45 | ServiceKey: "0", 46 | }}, 47 | } 48 | SamplePushoverReceiver = config.Receiver{ 49 | Name: "pushover_receiver", 50 | PushoverConfigs: []*config.PushoverConfig{{ 51 | UserKey: "101", 52 | Token: "0", 53 | }}, 54 | } 55 | SampleWebhookReceiver = config.Receiver{ 56 | Name: "webhook_receiver", 57 | WebhookConfigs: []*config.WebhookConfig{{ 58 | URL: &amconfig.URL{ 59 | URL: sampleURL, 60 | }, 61 | NotifierConfig: amconfig.NotifierConfig{ 62 | VSendResolved: true, 63 | }, 64 | }}, 65 | } 66 | SampleEmailReceiver = config.Receiver{ 67 | Name: "email_receiver", 68 | EmailConfigs: []*config.EmailConfig{{ 69 | To: "test@mail.com", 70 | From: "sampleUser", 71 | Headers: map[string]string{"header": "value"}, 72 | Smarthost: "http://mail-server.com", 73 | }}, 74 | } 75 | SampleConfig = config.Config{ 76 | Route: &SampleRoute, 77 | Receivers: []*config.Receiver{ 78 | &SampleSlackReceiver, &SampleReceiver, &SamplePagerDutyReceiver, &SamplePushoverReceiver, &SampleWebhookReceiver, &SampleEmailReceiver, 79 | }, 80 | } 81 | ) 82 | -------------------------------------------------------------------------------- /default_configs/alertmanager.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # This source code is licensed under the MIT license found in the 3 | # LICENSE file in the root directory of this source tree. 4 | 5 | global: 6 | resolve_timeout: 5m 7 | http_config: {} 8 | smtp_hello: localhost 9 | smtp_require_tls: true 10 | pagerduty_url: https://events.pagerduty.com/v2/enqueue 11 | opsgenie_api_url: https://api.opsgenie.com/ 12 | wechat_api_url: https://qyapi.weixin.qq.com/cgi-bin/ 13 | victorops_api_url: https://alert.victorops.com/integrations/generic/20131114/alert/ 14 | route: 15 | receiver: null_receiver 16 | group_by: 17 | - alertname 18 | group_wait: 10s 19 | group_interval: 10s 20 | repeat_interval: 1h 21 | receivers: 22 | - name: null_receiver 23 | templates: [] 24 | -------------------------------------------------------------------------------- /default_configs/prometheus.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # This source code is licensed under the MIT license found in the 3 | # LICENSE file in the root directory of this source tree. 4 | 5 | global: 6 | scrape_interval: 15s # By default, scrape targets every 15 seconds. 7 | evaluation_interval: 15s # By default, scrape targets every 15 seconds. 8 | external_labels: 9 | monitor: 'master' 10 | 11 | scrape_configs: 12 | - job_name: 'prometheus' 13 | static_configs: 14 | - targets: ['localhost:9090'] 15 | 16 | rule_files: 17 | - '/etc/prometheus/alert_rules/*_rules.yml' 18 | 19 | alerting: 20 | alertmanagers: 21 | - scheme: http 22 | static_configs: 23 | - targets: ['alertmanager:9093'] 24 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # This source code is licensed under the MIT license found in the 3 | # LICENSE file in the root directory of this source tree. 4 | 5 | version: '3.7' 6 | 7 | services: 8 | prom-configurer: 9 | container_name: prom-configurer 10 | build: 11 | context: . 12 | dockerfile: Dockerfile.prometheus 13 | command: 14 | - '-port=9100' 15 | - '-rules-dir=/etc/configs/alert_rules' 16 | - '-prometheusURL=prometheus:9090' 17 | - '-stderrthreshold=INFO' 18 | volumes: 19 | - ./default_configs:/etc/configs 20 | ports: 21 | - 9100:9100 22 | 23 | am-configurer: 24 | container_name: am-configurer 25 | build: 26 | context: . 27 | dockerfile: Dockerfile.alertmanager 28 | command: 29 | - '-port=9101' 30 | - '-alertmanager-conf=/etc/configs/alertmanager.yml' 31 | - '-alertmanagerURL=alertmanager:9093' 32 | - '-multitenant-label=tenant' 33 | - '-template-directory=/etc/configs/templates' 34 | - '-delete-route-with-receiver=true' 35 | - '-stderrthreshold=INFO' 36 | volumes: 37 | - ./default_configs:/etc/configs 38 | ports: 39 | - 9101:9101 40 | 41 | alertmanager: 42 | container_name: alertmanager 43 | image: docker.io/prom/alertmanager 44 | volumes: 45 | - ./default_configs:/etc/alertmanager:ro 46 | ports: 47 | - 9093:9093 48 | 49 | prometheus: 50 | container_name: prometheus 51 | image: docker.io/prom/prometheus 52 | volumes: 53 | - ./default_configs:/etc/prometheus:ro 54 | command: 55 | - '--config.file=/etc/prometheus/prometheus.yml' 56 | - '--web.enable-lifecycle' 57 | ports: 58 | - 9090:9090 59 | 60 | alerts-ui: 61 | container_name: alerts-ui 62 | build: 63 | context: ./ui 64 | dockerfile: Dockerfile 65 | stdin_open: true 66 | volumes: 67 | - './ui:/app' 68 | - '/app/node_modules' 69 | ports: 70 | - 3001:3000 71 | environment: 72 | - CHOKIDAR_USEPOLLING=true 73 | - REACT_APP_AM_BASE_URL=http://localhost:9093/api/v2 74 | - REACT_APP_AM_CONFIG_URL=http://localhost:9101/v1 75 | - REACT_APP_PROM_CONFIG_URL=http://localhost:9100/v1 76 | - REACT_APP_PROM_BASE_URL=http://localhost:9090/api/v1 77 | -------------------------------------------------------------------------------- /fsclient/fsclient.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package fsclient 9 | 10 | import ( 11 | "io/ioutil" 12 | "os" 13 | ) 14 | 15 | type FSClient interface { 16 | WriteFile(filename string, data []byte, perm os.FileMode) error 17 | ReadFile(filename string) ([]byte, error) 18 | DeleteFile(filename string) error 19 | Stat(filename string) (os.FileInfo, error) 20 | 21 | Root() string 22 | } 23 | 24 | type fsclient struct { 25 | root string 26 | } 27 | 28 | func NewFSClient(root string) FSClient { 29 | return &fsclient{ 30 | root: root, 31 | } 32 | } 33 | 34 | func (f *fsclient) WriteFile(filename string, data []byte, perm os.FileMode) error { 35 | return ioutil.WriteFile(f.root+filename, data, perm) 36 | } 37 | 38 | func (f *fsclient) ReadFile(filename string) ([]byte, error) { 39 | return ioutil.ReadFile(f.root + filename) 40 | } 41 | 42 | func (f *fsclient) DeleteFile(filename string) error { 43 | return os.Remove(f.root + filename) 44 | } 45 | 46 | func (f *fsclient) Stat(filename string) (os.FileInfo, error) { 47 | return os.Stat(f.root + filename) 48 | } 49 | 50 | func (f *fsclient) Root() string { 51 | return f.root 52 | } 53 | -------------------------------------------------------------------------------- /fsclient/mocks/FSClient.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | // Code generated by mockery v1.0.0. DO NOT EDIT. 9 | 10 | package mocks 11 | 12 | import ( 13 | os "os" 14 | 15 | mock "github.com/stretchr/testify/mock" 16 | ) 17 | 18 | // FSClient is an autogenerated mock type for the FSClient type 19 | type FSClient struct { 20 | mock.Mock 21 | } 22 | 23 | // DeleteFile provides a mock function with given fields: filename 24 | func (_m *FSClient) DeleteFile(filename string) error { 25 | ret := _m.Called(filename) 26 | 27 | var r0 error 28 | if rf, ok := ret.Get(0).(func(string) error); ok { 29 | r0 = rf(filename) 30 | } else { 31 | r0 = ret.Error(0) 32 | } 33 | 34 | return r0 35 | } 36 | 37 | // ReadFile provides a mock function with given fields: filename 38 | func (_m *FSClient) ReadFile(filename string) ([]byte, error) { 39 | ret := _m.Called(filename) 40 | 41 | var r0 []byte 42 | if rf, ok := ret.Get(0).(func(string) []byte); ok { 43 | r0 = rf(filename) 44 | } else { 45 | if ret.Get(0) != nil { 46 | r0 = ret.Get(0).([]byte) 47 | } 48 | } 49 | 50 | var r1 error 51 | if rf, ok := ret.Get(1).(func(string) error); ok { 52 | r1 = rf(filename) 53 | } else { 54 | r1 = ret.Error(1) 55 | } 56 | 57 | return r0, r1 58 | } 59 | 60 | // Root provides a mock function with given fields: 61 | func (_m *FSClient) Root() string { 62 | ret := _m.Called() 63 | 64 | var r0 string 65 | if rf, ok := ret.Get(0).(func() string); ok { 66 | r0 = rf() 67 | } else { 68 | r0 = ret.Get(0).(string) 69 | } 70 | 71 | return r0 72 | } 73 | 74 | // Stat provides a mock function with given fields: filename 75 | func (_m *FSClient) Stat(filename string) (os.FileInfo, error) { 76 | ret := _m.Called(filename) 77 | 78 | var r0 os.FileInfo 79 | if rf, ok := ret.Get(0).(func(string) os.FileInfo); ok { 80 | r0 = rf(filename) 81 | } else { 82 | if ret.Get(0) != nil { 83 | r0 = ret.Get(0).(os.FileInfo) 84 | } 85 | } 86 | 87 | var r1 error 88 | if rf, ok := ret.Get(1).(func(string) error); ok { 89 | r1 = rf(filename) 90 | } else { 91 | r1 = ret.Error(1) 92 | } 93 | 94 | return r0, r1 95 | } 96 | 97 | // WriteFile provides a mock function with given fields: filename, data, perm 98 | func (_m *FSClient) WriteFile(filename string, data []byte, perm os.FileMode) error { 99 | ret := _m.Called(filename, data, perm) 100 | 101 | var r0 error 102 | if rf, ok := ret.Get(0).(func(string, []byte, os.FileMode) error); ok { 103 | r0 = rf(filename, data, perm) 104 | } else { 105 | r0 = ret.Error(0) 106 | } 107 | 108 | return r0 109 | } 110 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/facebookincubator/prometheus-configmanager 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b 7 | github.com/imdario/mergo v0.3.5 8 | github.com/labstack/echo v0.0.0-20181123063414-c54d9e8eed6c 9 | github.com/labstack/gommon v0.2.8 // indirect 10 | github.com/pkg/errors v0.9.1 11 | github.com/prometheus/alertmanager v0.21.0 12 | github.com/prometheus/common v0.11.1 13 | github.com/prometheus/prometheus v1.8.2-0.20200819132913-cb830b0a9c78 14 | github.com/prometheus/tsdb v0.10.0 // indirect 15 | github.com/stretchr/testify v1.5.1 16 | github.com/thoas/go-funk v0.4.0 17 | gopkg.in/yaml.v2 v2.3.0 18 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 19 | ) 20 | -------------------------------------------------------------------------------- /helm/prometheus-configmanager/Chart.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # This source code is licensed under the MIT license found in the 3 | # LICENSE file in the root directory of this source tree. 4 | 5 | name: prometheus-configmanager 6 | apiVersion: v1 7 | version: 0.1.1 8 | description: Prometheus configmanager which provides an API to update prometheus and alertmanager configurations during runtime. 9 | keywords: 10 | - prometheus 11 | maintainers: 12 | - name: Scott Smith 13 | email: smithscott@fb.com 14 | engine: gotpl 15 | -------------------------------------------------------------------------------- /helm/prometheus-configmanager/templates/alertmanager-configurer.deployment.yaml: -------------------------------------------------------------------------------- 1 | {{/* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */}} 7 | {{- if .Values.alertmanagerConfigurer.create }} 8 | apiVersion: apps/v1 9 | kind: Deployment 10 | metadata: 11 | name: alertmanager-configurer 12 | labels: 13 | app.kubernetes.io/component: alertmanager-configurer 14 | spec: 15 | replicas: {{ .Values.alertmanagerConfigurer.replicas }} 16 | selector: 17 | matchLabels: 18 | app.kubernetes.io/component: alertmanager-configurer 19 | template: 20 | metadata: 21 | labels: 22 | app.kubernetes.io/component: alertmanager-configurer 23 | spec: 24 | {{- with .Values.alertmanagerConfigurer.nodeSelector }} 25 | nodeSelector: 26 | {{ toYaml . | indent 8 }} 27 | {{- end }} 28 | {{- with .Values.alertmanagerConfigurer.tolerations }} 29 | tolerations: 30 | {{ toYaml . | indent 8 }} 31 | {{- end }} 32 | {{- with .Values.alertmanagerConfigurer.affinity }} 33 | affinity: 34 | {{ toYaml . | indent 8 }} 35 | {{- end }} 36 | {{- with .Values.imagePullSecrets }} 37 | imagePullSecrets: 38 | {{ toYaml . | trimSuffix "\n" | indent 8 }} 39 | {{- end }} 40 | 41 | volumes: 42 | - name: "prometheus-config" 43 | {{- with .Values.volumes.prometheusConfig.volumeSpec }} 44 | {{ toYaml .Values.volumes.prometheusConfig.volumeSpec | indent 10 }} 45 | {{- end }} 46 | 47 | containers: 48 | - name: "alertmanager-configurer" 49 | image: {{ required "alertmanagerConfigurer.image.repository must be provided" .Values.alertmanagerConfigurer.image.repository }}:{{ .Values.alertmanagerConfigurer.image.tag }} 50 | imagePullPolicy: {{ .Values.alertmanagerConfigurer.image.pullPolicy }} 51 | ports: 52 | - containerPort: 9101 53 | volumeMounts: 54 | - name: "prometheus-config" 55 | mountPath: /etc/configs 56 | args: 57 | - "-port={{ .Values.alertmanagerConfigurer.alertmanagerConfigPort }}" 58 | - "-alertmanager-conf={{ .Values.alertmanagerConfigurer.alertmanagerConfPath }}" 59 | - "-alertmanagerURL={{ .Values.alertmanagerConfigurer.alertmanagerURL }}" 60 | {{- if .Values.alertmanagerConfigurer.multitenantLabel }} 61 | - "-multitenant-label={{ .Values.alertmanagerConfigurer.multitenantLabel }}" 62 | {{- end }} 63 | resources: 64 | {{ toYaml .Values.alertmanagerConfigurer.resources | indent 12 }} 65 | {{- end }} 66 | -------------------------------------------------------------------------------- /helm/prometheus-configmanager/templates/alertmanager-configurer.service.yaml: -------------------------------------------------------------------------------- 1 | {{/* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree.. 6 | */}} 7 | {{- if .Values.alertmanagerConfigurer.create }} 8 | apiVersion: v1 9 | kind: Service 10 | metadata: 11 | name: alertmanager-configurer 12 | labels: 13 | app.kubernetes.io/component: alertmanager-configurer 14 | {{- with .Values.alertmanagerConfigurer.service.labels }} 15 | {{ toYaml . | indent 4}} 16 | {{- end}} 17 | {{- with .Values.alertmanagerConfigurer.service.annotations }} 18 | annotations: 19 | {{ toYaml . | indent 4}} 20 | {{- end }} 21 | spec: 22 | selector: 23 | app.kubernetes.io/component: alertmanager-configurer 24 | type: {{ .Values.alertmanagerConfigurer.service.type }} 25 | ports: 26 | {{- range $port := .Values.alertmanagerConfigurer.service.ports }} 27 | - name: {{ $port.name }} 28 | port: {{ $port.port }} 29 | targetPort: {{ $port.targetPort }} 30 | {{- end }} 31 | {{- if eq .Values.alertmanagerConfigurer.service.type "LoadBalancer" }} 32 | {{- if .Values.alertmanagerConfigurer.service.loadBalancerIP }} 33 | loadBalancerIP: {{ .Values.alertmanagerConfigurer.service.loadBalancerIP }} 34 | {{- end -}} 35 | {{- if .Values.alertmanagerConfigurer.service.loadBalancerSourceRanges }} 36 | loadBalancerSourceRanges: 37 | {{- range .Values.alertmanagerConfigurer.service.loadBalancerSourceRanges }} 38 | - {{ . }} 39 | {{- end }} 40 | {{- end -}} 41 | {{- end -}} 42 | {{- end }} 43 | -------------------------------------------------------------------------------- /helm/prometheus-configmanager/templates/alerts-ui.deployment.yaml: -------------------------------------------------------------------------------- 1 | {{/* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */}} 7 | {{- if .Values.alertsUI.create }} 8 | apiVersion: apps/v1 9 | kind: Deployment 10 | metadata: 11 | name: alerts-ui 12 | labels: 13 | app.kubernetes.io/component: alerts-ui 14 | spec: 15 | replicas: {{ .Values.alertsUI.replicas }} 16 | selector: 17 | matchLabels: 18 | app.kubernetes.io/component: alerts-ui 19 | template: 20 | metadata: 21 | labels: 22 | app.kubernetes.io/component: alerts-ui 23 | spec: 24 | {{- with .Values.alertsUI.nodeSelector }} 25 | nodeSelector: 26 | {{ toYaml . | indent 8 }} 27 | {{- end }} 28 | {{- with .Values.alertsUI.tolerations }} 29 | tolerations: 30 | {{ toYaml . | indent 8 }} 31 | {{- end }} 32 | {{- with .Values.alertsUI.affinity }} 33 | affinity: 34 | {{ toYaml . | indent 8 }} 35 | {{- end }} 36 | {{- with .Values.imagePullSecrets }} 37 | imagePullSecrets: 38 | {{ toYaml . | trimSuffix "\n" | indent 8 }} 39 | {{- end }} 40 | 41 | containers: 42 | - name: "alerts-ui" 43 | image: {{ required "alertsUI.image.repository must be provided" .Values.alertsUI.image.repository }}:{{ .Values.alertsUI.image.tag }} 44 | imagePullPolicy: {{ .Values.alertsUI.image.pullPolicy }} 45 | ports: 46 | - containerPort: 3000 47 | env: 48 | - name: "REACT_APP_AM_BASE_URL" 49 | value: {{ .Values.alertsUI.alertmanager_address}}/api/v2 50 | - name: "REACT_APP_PROM_BASE_URL" 51 | value: {{ .Values.alertsUI.prometheus_address}}/api/v1 52 | - name: "REACT_APP_AM_CONFIG_URL" 53 | value: alertmanager-configurer:{{ .Values.alertmanagerConfigurer.port }}/v1 54 | - name: "REACT_APP_PROM_CONFIG_URL" 55 | value: prometheus-configrer:{{ .Values.prometheusConfigurer.port }}/v1 56 | resources: 57 | {{ toYaml .Values.alertsUI.resources | indent 12 }} 58 | {{- end }} 59 | -------------------------------------------------------------------------------- /helm/prometheus-configmanager/templates/alerts-ui.service.yaml: -------------------------------------------------------------------------------- 1 | {{/* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree.. 6 | */}} 7 | {{- if .Values.alertsUI.create }} 8 | apiVersion: v1 9 | kind: Service 10 | metadata: 11 | name: alerts-ui 12 | labels: 13 | app.kubernetes.io/component: alerts-ui 14 | {{- with .Values.alertsUI.service.labels }} 15 | {{ toYaml . | indent 4}} 16 | {{- end}} 17 | {{- with .Values.alertsUI.service.annotations }} 18 | annotations: 19 | {{ toYaml . | indent 4}} 20 | {{- end }} 21 | spec: 22 | selector: 23 | app.kubernetes.io/component: alerts-ui 24 | type: {{ .Values.alertsUI.service.type }} 25 | ports: 26 | {{- range $port := .Values.alertsUI.service.ports }} 27 | - name: {{ $port.name }} 28 | port: {{ $port.port }} 29 | targetPort: {{ $port.targetPort }} 30 | {{- end }} 31 | {{- if eq .Values.alertsUI.service.type "LoadBalancer" }} 32 | {{- if .Values.alertsUI.service.loadBalancerIP }} 33 | loadBalancerIP: {{ .Values.alertsUI.service.loadBalancerIP }} 34 | {{- end -}} 35 | {{- if .Values.alertsUI.service.loadBalancerSourceRanges }} 36 | loadBalancerSourceRanges: 37 | {{- range .Values.alertsUI.service.loadBalancerSourceRanges }} 38 | - {{ . }} 39 | {{- end }} 40 | {{- end -}} 41 | {{- end -}} 42 | {{- end }} 43 | -------------------------------------------------------------------------------- /helm/prometheus-configmanager/templates/prometheus-configurer.deployment.yaml: -------------------------------------------------------------------------------- 1 | {{/* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */}} 7 | {{- if .Values.prometheusConfigurer.create }} 8 | apiVersion: apps/v1 9 | kind: Deployment 10 | metadata: 11 | name: prometheus-configurer 12 | labels: 13 | app.kubernetes.io/component: prometheus-configurer 14 | spec: 15 | replicas: {{ .Values.prometheusConfigurer.replicas }} 16 | selector: 17 | matchLabels: 18 | app.kubernetes.io/component: prometheus-configurer 19 | template: 20 | metadata: 21 | labels: 22 | app.kubernetes.io/component: prometheus-configurer 23 | spec: 24 | {{- with .Values.prometheusConfigurer.nodeSelector }} 25 | nodeSelector: 26 | {{ toYaml . | indent 8 }} 27 | {{- end }} 28 | {{- with .Values.prometheusConfigurer.tolerations }} 29 | tolerations: 30 | {{ toYaml . | indent 8 }} 31 | {{- end }} 32 | {{- with .Values.prometheusConfigurer.affinity }} 33 | affinity: 34 | {{ toYaml . | indent 8 }} 35 | {{- end }} 36 | {{- with .Values.imagePullSecrets }} 37 | imagePullSecrets: 38 | {{ toYaml . | trimSuffix "\n" | indent 8 }} 39 | {{- end }} 40 | 41 | volumes: 42 | - name: "prometheus-config" 43 | {{- with .Values.volumes.prometheusConfig.volumeSpec }} 44 | {{ toYaml .Values.volumes.prometheusConfig.volumeSpec | indent 10 }} 45 | {{- end }} 46 | 47 | containers: 48 | - name: "prometheus-configurer" 49 | image: {{ required "prometheusConfigurer.image.repository must be provided" .Values.prometheusConfigurer.image.repository }}:{{ .Values.prometheusConfigurer.image.tag }} 50 | imagePullPolicy: {{ .Values.prometheusConfigurer.image.pullPolicy }} 51 | ports: 52 | - containerPort: 9100 53 | volumeMounts: 54 | - name: "prometheus-config" 55 | mountPath: /etc/configs 56 | args: 57 | - "-port={{ .Values.prometheusConfigurer.prometheusConfigurerPort }}" 58 | - "-rules-dir={{ .Values.prometheusConfigurer.rulesDir }}" 59 | - "-prometheusURL={{ .Values.prometheusConfigurer.prometheusURL }}" 60 | {{- if .Values.prometheusConfigurer.multitenantLabel }} 61 | - "-multitenant-label={{ .Values.prometheusConfigurer.multitenantLabel }}" 62 | {{- end }} 63 | {{- if .Values.prometheusConfigurer.restrictQueries }} 64 | - "-restrict-queries" 65 | {{- end }} 66 | resources: 67 | {{ toYaml .Values.prometheusConfigurer.resources | indent 12 }} 68 | {{- end }} 69 | -------------------------------------------------------------------------------- /helm/prometheus-configmanager/templates/prometheus-configurer.service.yaml: -------------------------------------------------------------------------------- 1 | {{/* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */}} 7 | {{- if .Values.prometheusConfigurer.create }} 8 | apiVersion: v1 9 | kind: Service 10 | metadata: 11 | name: prometheus-configurer 12 | labels: 13 | app.kubernetes.io/component: prometheus-configurer 14 | {{- with .Values.prometheusConfigurer.service.labels }} 15 | {{ toYaml . | indent 4}} 16 | {{- end}} 17 | {{- with .Values.prometheusConfigurer.service.annotations }} 18 | annotations: 19 | {{ toYaml . | indent 4}} 20 | {{- end }} 21 | spec: 22 | selector: 23 | app.kubernetes.io/component: prometheus-configurer 24 | type: {{ .Values.prometheusConfigurer.service.type }} 25 | ports: 26 | {{- range $port := .Values.prometheusConfigurer.service.ports }} 27 | - name: {{ $port.name }} 28 | port: {{ $port.port }} 29 | targetPort: {{ $port.targetPort }} 30 | {{- end }} 31 | {{- if eq .Values.prometheusConfigurer.service.type "LoadBalancer" }} 32 | {{- if .Values.prometheusConfigurer.service.loadBalancerIP }} 33 | loadBalancerIP: {{ .Values.prometheusConfigurer.service.loadBalancerIP }} 34 | {{- end -}} 35 | {{- if .Values.prometheusConfigurer.service.loadBalancerSourceRanges }} 36 | loadBalancerSourceRanges: 37 | {{- range .Values.prometheusConfigurer.service.loadBalancerSourceRanges }} 38 | - {{ . }} 39 | {{- end }} 40 | {{- end -}} 41 | {{- end -}} 42 | {{- end }} 43 | -------------------------------------------------------------------------------- /helm/prometheus-configmanager/values.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # This source code is licensed under the MIT license found in the 3 | # LICENSE file in the root directory of this source tree. 4 | 5 | # Reference to one or more secrets to be used when pulling images 6 | # ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ 7 | imagePullSecrets: [] 8 | 9 | volumes: 10 | prometheusConfig: 11 | volumeSpec: {} 12 | # hostPath: 13 | # path: /configs/prometheus 14 | # type: DirectoryOrCreate 15 | 16 | prometheusConfigurer: 17 | # Enable/Disable chart 18 | create: true 19 | 20 | replicas: 1 21 | 22 | port: &port 9100 23 | 24 | # Metric Label used for restricting alerts per-tenant 25 | multitenantLabel: "" 26 | 27 | # Boolean to set whether to restrict alert expressions with multitenantLabel 28 | restrictQueries: false 29 | 30 | service: 31 | annotations: {} 32 | labels: {} 33 | type: ClusterIP 34 | ports: 35 | - name: prom-configmanager 36 | port: *port 37 | targetPort: *port 38 | 39 | prometheusConfigurerPort: *port 40 | rulesDir: "/etc/configs/alert_rules" 41 | prometheusURL: "prometheus:9090" 42 | 43 | image: 44 | repository: prometheus-configurer 45 | tag: latest 46 | pullPolicy: IfNotPresent 47 | 48 | resources: {} 49 | nodeSelector: {} 50 | tolerations: [] 51 | 52 | # Pod affinity must be used to ensure that this pod runs on same node as prometheus 53 | 54 | # Use the following in your values.yml file to ensure that this pod runs on the same 55 | # node as prometheus: 56 | affinity: {} 57 | # affinity: 58 | # podAffinity: 59 | # requiredDuringSchedulingIgnoredDuringExecution: 60 | # - labelSelector: 61 | # matchExpressions: 62 | # - key: app.kubernetes.io/component 63 | # operator: In 64 | # values: 65 | # - prometheus 66 | # topologyKey: "kubernetes.io/hostname" 67 | 68 | alertmanagerConfigurer: 69 | # Enable/Disable chart 70 | create: true 71 | 72 | replicas: 1 73 | 74 | port: &port 9091 75 | 76 | # Metric Label used for restricting alerts per-tenant 77 | multitenantLabel: "" 78 | 79 | service: 80 | annotations: {} 81 | labels: {} 82 | type: ClusterIP 83 | ports: 84 | - name: alertmanager-config 85 | port: *port 86 | targetPort: *port 87 | 88 | alertmanagerConfigPort: *port 89 | alertmanagerConfPath: "/etc/configs/alertmanager.yml" 90 | alertmanagerURL: "alertmanager:9093" 91 | 92 | image: 93 | repository: alertmanager-configurer 94 | tag: latest 95 | pullPolicy: IfNotPresent 96 | 97 | resources: {} 98 | nodeSelector: {} 99 | tolerations: [] 100 | 101 | # Pod affinity must be used to ensure that this pod runs on same node as prometheus 102 | # 103 | # Use the following in your values.yml file to ensure that this pod runs on the same 104 | # node as prometheus: 105 | 106 | # affinity: 107 | # podAffinity: 108 | # requiredDuringSchedulingIgnoredDuringExecution: 109 | # - labelSelector: 110 | # matchExpressions: 111 | # - key: app.kubernetes.io/component 112 | # operator: In 113 | # values: 114 | # - prometheus 115 | # topologyKey: "kubernetes.io/hostname" 116 | affinity: {} 117 | 118 | alertsUI: 119 | # Enable/Disable chart 120 | create: true 121 | 122 | replicas: 1 123 | 124 | ui_port: &am_port 9091 125 | 126 | alertmanager_address: 'alertmanager:9093' 127 | prometheus_address: 'prometheus:9090' 128 | 129 | service: 130 | annotations: {} 131 | labels: {} 132 | type: ClusterIP 133 | ports: 134 | - name: alertmanager-config 135 | port: 3001 136 | targetPort: 3001 137 | 138 | image: 139 | repository: alerts-ui 140 | tag: latest 141 | pullPolicy: IfNotPresent 142 | 143 | resources: {} 144 | nodeSelector: {} 145 | tolerations: [] 146 | 147 | # Pod affinity must be used to ensure that this pod runs on same node as prometheus 148 | 149 | # Use the following in your values.yml file to ensure that this pod runs on the same 150 | # node as prometheus: 151 | 152 | affinity: {} 153 | # affinity: 154 | # podAffinity: 155 | # requiredDuringSchedulingIgnoredDuringExecution: 156 | # - labelSelector: 157 | # matchExpressions: 158 | # - key: app.kubernetes.io/component 159 | # operator: In 160 | # values: 161 | # - prometheus 162 | # topologyKey: "kubernetes.io/hostname 163 | -------------------------------------------------------------------------------- /prometheus/alert/alert_rule.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package alert 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/facebookincubator/prometheus-configmanager/restrictor" 14 | 15 | "github.com/prometheus/common/model" 16 | "github.com/prometheus/prometheus/pkg/rulefmt" 17 | ) 18 | 19 | type File struct { 20 | RuleGroups []RuleGroup `yaml:"groups"` 21 | } 22 | 23 | // RuleGroup holds the fields in a Prometheus Alert Rule Group 24 | type RuleGroup struct { 25 | Name string `yaml:"name"` 26 | Interval model.Duration `yaml:"interval,omitempty"` 27 | Rules []rulefmt.Rule `yaml:"rules"` 28 | } 29 | 30 | func NewFile(tenantID string) *File { 31 | return &File{ 32 | RuleGroups: []RuleGroup{{ 33 | Name: tenantID, 34 | }}, 35 | } 36 | } 37 | 38 | // Rules returns the rule configs from this file 39 | func (f *File) Rules() []rulefmt.Rule { 40 | return f.RuleGroups[0].Rules 41 | } 42 | 43 | // GetRule returns the specific rule by name. Nil if it isn't found 44 | func (f *File) GetRule(rulename string) *rulefmt.Rule { 45 | for _, rule := range f.RuleGroups[0].Rules { 46 | if rule.Alert == rulename { 47 | return &rule 48 | } 49 | } 50 | return nil 51 | } 52 | 53 | // AddRule appends a new rule to the list of rules in this file 54 | func (f *File) AddRule(rule rulefmt.Rule) { 55 | f.RuleGroups[0].Rules = append(f.RuleGroups[0].Rules, rule) 56 | } 57 | 58 | // ReplaceRule replaces an existing rule. Returns error if rule does not 59 | // exist already 60 | func (f *File) ReplaceRule(newRule rulefmt.Rule) error { 61 | ruleIdx := -1 62 | for idx, rule := range f.RuleGroups[0].Rules { 63 | if rule.Alert == newRule.Alert { 64 | ruleIdx = idx 65 | } 66 | } 67 | if ruleIdx < 0 { 68 | return fmt.Errorf("rule %s does not exist", newRule.Alert) 69 | } 70 | 71 | f.RuleGroups[0].Rules[ruleIdx] = newRule 72 | return nil 73 | } 74 | 75 | func (f *File) DeleteRule(name string) error { 76 | rules := f.RuleGroups[0].Rules 77 | for idx, rule := range rules { 78 | if rule.Alert == name { 79 | f.RuleGroups[0].Rules = append(rules[:idx], rules[idx+1:]...) 80 | return nil 81 | } 82 | } 83 | return fmt.Errorf("alert with name %s not found", name) 84 | } 85 | 86 | // SecureRule attaches a label for tenantID to the given alert expression to 87 | // to ensure that only metrics owned by this tenant can be alerted on 88 | func SecureRule(restrictQueries bool, matcherName, matcherValue string, rule *rulefmt.Rule) error { 89 | expr := rule.Expr 90 | var err error 91 | if restrictQueries { 92 | queryRestrictor := restrictor.NewQueryRestrictor(restrictor.DefaultOpts).AddMatcher(matcherName, matcherValue) 93 | expr, err = queryRestrictor.RestrictQuery(rule.Expr) 94 | if err != nil { 95 | return err 96 | } 97 | } 98 | 99 | rule.Expr = expr 100 | if rule.Labels == nil { 101 | rule.Labels = make(map[string]string) 102 | } 103 | rule.Labels[matcherName] = matcherValue 104 | return nil 105 | } 106 | 107 | // RuleJSONWrapper Provides a struct to marshal/unmarshal into a rulefmt.Rule 108 | // since rulefmt does not support json encoding 109 | type RuleJSONWrapper struct { 110 | Record string `json:"record,omitempty"` 111 | Alert string `json:"alert,omitempty"` 112 | Expr string `json:"expr"` 113 | For string `json:"for,omitempty"` 114 | Labels map[string]string `json:"labels,omitempty"` 115 | Annotations map[string]string `json:"annotations,omitempty"` 116 | } 117 | 118 | func (r *RuleJSONWrapper) ToRuleFmt() (rulefmt.Rule, error) { 119 | if r.Labels == nil { 120 | r.Labels = make(map[string]string) 121 | } 122 | if r.Annotations == nil { 123 | r.Annotations = make(map[string]string) 124 | } 125 | 126 | rule := rulefmt.Rule{ 127 | Record: r.Record, 128 | Alert: r.Alert, 129 | Expr: r.Expr, 130 | Labels: r.Labels, 131 | Annotations: r.Annotations, 132 | } 133 | if r.For != "" { 134 | modelFor, err := model.ParseDuration(r.For) 135 | if err != nil { 136 | return rulefmt.Rule{}, err 137 | } 138 | rule.For = modelFor 139 | } 140 | return rule, nil 141 | } 142 | -------------------------------------------------------------------------------- /prometheus/alert/alert_rule_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package alert_test 9 | 10 | import ( 11 | "testing" 12 | 13 | "github.com/facebookincubator/prometheus-configmanager/prometheus/alert" 14 | "github.com/facebookincubator/prometheus-configmanager/restrictor" 15 | 16 | "github.com/prometheus/common/model" 17 | "github.com/prometheus/prometheus/pkg/rulefmt" 18 | "github.com/stretchr/testify/assert" 19 | ) 20 | 21 | const ( 22 | alertName = "testAlert" 23 | alertName2 = "testAlert2" 24 | ) 25 | 26 | var ( 27 | sampleRule = rulefmt.Rule{ 28 | Alert: alertName, 29 | Expr: "up == 0", 30 | Labels: map[string]string{"name": "value"}, 31 | } 32 | 33 | sampleRule2 = rulefmt.Rule{ 34 | Alert: alertName2, 35 | Expr: "up == 0", 36 | Labels: map[string]string{"name": "value"}, 37 | } 38 | ) 39 | 40 | func TestFile_GetRule(t *testing.T) { 41 | f := sampleFile() 42 | 43 | rule := f.GetRule(alertName) 44 | assert.Equal(t, sampleRule, *rule) 45 | 46 | rule = f.GetRule("") 47 | assert.Nil(t, rule) 48 | } 49 | 50 | func TestFile_AddRule(t *testing.T) { 51 | f := sampleFile() 52 | 53 | f.AddRule(sampleRule2) 54 | assert.Equal(t, 2, len(f.Rules())) 55 | assert.NotNil(t, f.GetRule(alertName)) 56 | assert.NotNil(t, f.GetRule(alertName2)) 57 | } 58 | 59 | func TestFile_ReplaceRule(t *testing.T) { 60 | f := sampleFile() 61 | newRule := rulefmt.Rule{ 62 | Alert: alertName, 63 | Expr: "up == 1", 64 | } 65 | err := f.ReplaceRule(newRule) 66 | assert.NoError(t, err) 67 | assert.Equal(t, 1, len(f.Rules())) 68 | assert.Equal(t, newRule, *f.GetRule(alertName)) 69 | 70 | badRule := rulefmt.Rule{ 71 | Alert: "badRule", 72 | } 73 | 74 | err = f.ReplaceRule(badRule) 75 | assert.Error(t, err) 76 | } 77 | 78 | func TestFile_DeleteRule(t *testing.T) { 79 | f := sampleFile() 80 | err := f.DeleteRule(alertName) 81 | assert.NoError(t, err) 82 | assert.Equal(t, 0, len(f.Rules())) 83 | 84 | // error if deleting non-existent rule 85 | err = f.DeleteRule(alertName) 86 | assert.Error(t, err) 87 | } 88 | 89 | func TestSecureRule(t *testing.T) { 90 | rule := sampleRule 91 | err := alert.SecureRule(true, "tenantID", "test", &rule) 92 | assert.NoError(t, err) 93 | 94 | queryRestrictor := restrictor.NewQueryRestrictor(restrictor.DefaultOpts).AddMatcher("tenantID", "test") 95 | expectedExpr, _ := queryRestrictor.RestrictQuery(sampleRule.Expr) 96 | 97 | assert.Equal(t, expectedExpr, rule.Expr) 98 | assert.Equal(t, 2, len(rule.Labels)) 99 | assert.Equal(t, "test", rule.Labels["tenantID"]) 100 | 101 | existingNetworkIDRule := rulefmt.Rule{ 102 | Alert: alertName2, 103 | Expr: `up{tenantID="test"} == 0`, 104 | Labels: map[string]string{"name": "value", "tenantID": "test"}, 105 | } 106 | restricted, _ := queryRestrictor.RestrictQuery(existingNetworkIDRule.Expr) 107 | // assert tenantID isn't appended twice 108 | assert.Equal(t, expectedExpr, restricted) 109 | assert.Equal(t, 2, len(rule.Labels)) 110 | 111 | // assert expression is not restricted when restrictQueries is false 112 | rule = sampleRule 113 | origRule := sampleRule.Expr 114 | err = alert.SecureRule(false, "tenantID", "test", &rule) 115 | assert.NoError(t, err) 116 | assert.Equal(t, origRule, rule.Expr) 117 | 118 | rule = rulefmt.Rule{ 119 | Alert: "test", 120 | Expr: "up == 0", 121 | } 122 | restricted, _ = queryRestrictor.RestrictQuery(rule.Expr) 123 | err = alert.SecureRule(true, "tenantID", "test", &rule) 124 | assert.NoError(t, err) 125 | assert.Equal(t, restricted, rule.Expr) 126 | assert.Equal(t, 1, len(rule.Labels)) 127 | assert.Equal(t, "test", rule.Labels["tenantID"]) 128 | } 129 | 130 | func TestRuleJSONWrapper_ToRuleFmt(t *testing.T) { 131 | jsonRule := alert.RuleJSONWrapper{ 132 | Record: "record", 133 | Alert: "alert", 134 | Expr: "expr", 135 | For: "5s", 136 | Labels: nil, 137 | Annotations: nil, 138 | } 139 | 140 | expectedFor, _ := model.ParseDuration("5s") 141 | expectedRule := rulefmt.Rule{ 142 | Record: "record", 143 | Alert: "alert", 144 | Expr: "expr", 145 | For: expectedFor, 146 | Labels: map[string]string{}, 147 | Annotations: map[string]string{}, 148 | } 149 | 150 | actualRule, err := jsonRule.ToRuleFmt() 151 | assert.NoError(t, err) 152 | assert.Equal(t, expectedRule, actualRule) 153 | } 154 | 155 | func sampleFile() alert.File { 156 | return alert.File{ 157 | RuleGroups: []alert.RuleGroup{{ 158 | Name: "testGroup", 159 | Rules: []rulefmt.Rule{sampleRule}, 160 | }, 161 | }, 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /prometheus/alert/client_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package alert_test 9 | 10 | import ( 11 | "errors" 12 | "testing" 13 | 14 | "github.com/facebookincubator/prometheus-configmanager/fsclient/mocks" 15 | "github.com/facebookincubator/prometheus-configmanager/prometheus/alert" 16 | 17 | "github.com/prometheus/common/model" 18 | "github.com/prometheus/prometheus/pkg/rulefmt" 19 | "github.com/stretchr/testify/assert" 20 | "github.com/stretchr/testify/mock" 21 | ) 22 | 23 | const ( 24 | testNID = "test" 25 | testRuleFile = `groups: 26 | - name: test 27 | rules: 28 | - alert: test_rule_1 29 | expr: up == 0{tenantID="test"} 30 | for: 5s 31 | labels: 32 | severity: major 33 | tenantID: test 34 | - alert: test_rule_2 35 | expr: up == 1{tenantID="test"} 36 | for: 5s 37 | labels: 38 | severity: critical 39 | tenantID: test 40 | annotations: 41 | summary: A test rule` 42 | 43 | otherNID = "other" 44 | otherRuleFile = `groups: 45 | - name: other 46 | rules: 47 | - alert: other_rule_1 48 | expr: up == 0{tenantID="other"} 49 | for: 5s 50 | labels: 51 | severity: major 52 | tenantID: other 53 | - alert: test_rule_2 54 | expr: up == 1{tenantID="other"} 55 | for: 5s 56 | labels: 57 | severity: critical 58 | tenantID: other 59 | annotations: 60 | summary: A test rule` 61 | ) 62 | 63 | var ( 64 | fiveSeconds, _ = model.ParseDuration("5s") 65 | testRule1 = rulefmt.Rule{ 66 | Alert: "test_rule_1", 67 | Expr: "up==0", 68 | For: fiveSeconds, 69 | Labels: map[string]string{"severity": "major", "tenantID": testNID}, 70 | } 71 | badRule = rulefmt.Rule{ 72 | Alert: "bad_rule", 73 | Expr: "malformed{.}", 74 | } 75 | 76 | // Mock FSClient that never errs 77 | healthyFSClient = newFSClient(nil, nil) 78 | readErrFSClient = newFSClient(errors.New("read err"), nil) 79 | writeErrFSClient = newFSClient(nil, errors.New("write err")) 80 | ) 81 | 82 | type validateRuleTestCase struct { 83 | name string 84 | rule rulefmt.Rule 85 | expectedError string 86 | } 87 | 88 | func (tc *validateRuleTestCase) RunTest(t *testing.T) { 89 | err := alert.ValidateRule(tc.rule) 90 | if tc.expectedError == "" { 91 | assert.NoError(t, err) 92 | return 93 | } 94 | assert.EqualError(t, err, tc.expectedError) 95 | } 96 | 97 | func TestValidateRule(t *testing.T) { 98 | tests := []validateRuleTestCase{ 99 | { 100 | name: "valid rule", 101 | rule: rulefmt.Rule{ 102 | Alert: "test", 103 | Expr: "up", 104 | For: 0, 105 | Labels: map[string]string{"label1": "value"}, 106 | Annotations: map[string]string{"annotation1": "value"}, 107 | }, 108 | }, 109 | { 110 | name: "record and alert defined", 111 | rule: rulefmt.Rule{Alert: "alert", Record: "record"}, 112 | expectedError: "Rule Validation Error; only one of 'record' and 'alert' must be set; field 'expr' must be set in rule", 113 | }, 114 | { 115 | name: "neither defined", 116 | rule: rulefmt.Rule{Alert: "", Record: ""}, 117 | expectedError: "Rule Validation Error; one of 'record' or 'alert' must be set; field 'expr' must be set in rule", 118 | }, 119 | { 120 | name: "no expression", 121 | rule: rulefmt.Rule{Alert: "test", Expr: ""}, 122 | expectedError: "Rule Validation Error; field 'expr' must be set in rule", 123 | }, 124 | { 125 | name: "invalid expression", 126 | rule: rulefmt.Rule{Alert: "test", Expr: "!up"}, 127 | expectedError: "Rule Validation Error; could not parse expression: 1:1: parse error: unexpected character after '!': 'u'", 128 | }, 129 | { 130 | name: "annotions in recording rule", 131 | rule: rulefmt.Rule{Record: "test", Expr: "up", Annotations: map[string]string{"a": "b"}}, 132 | expectedError: "Rule Validation Error; invalid field 'annotations' in recording rule", 133 | }, 134 | { 135 | name: "invalid recording rule name", 136 | rule: rulefmt.Rule{Record: "1test", Expr: "up"}, 137 | expectedError: "Rule Validation Error; invalid recording rule name: 1test", 138 | }, 139 | { 140 | name: "invalid label name", 141 | rule: rulefmt.Rule{Alert: "test", Expr: "up", Labels: map[string]string{"1label": "val"}}, 142 | expectedError: "Rule Validation Error; invalid label name: 1label", 143 | }, 144 | { 145 | name: "invalid annotation name", 146 | rule: rulefmt.Rule{Alert: "test", Expr: "up", Annotations: map[string]string{"1label": "val"}}, 147 | expectedError: "Rule Validation Error; invalid annotation name: 1label", 148 | }, 149 | } 150 | 151 | for _, tc := range tests { 152 | t.Run(tc.name, tc.RunTest) 153 | } 154 | } 155 | 156 | func TestClient_RuleExists(t *testing.T) { 157 | client := newTestClient("tenantID", healthyFSClient) 158 | assert.True(t, client.RuleExists(testNID, "test_rule_1")) 159 | assert.True(t, client.RuleExists(testNID, "test_rule_2")) 160 | assert.False(t, client.RuleExists(testNID, "no_rule")) 161 | assert.False(t, client.RuleExists(testNID, "other_rule_1")) 162 | 163 | assert.True(t, client.RuleExists(otherNID, "other_rule_1")) 164 | assert.True(t, client.RuleExists(otherNID, "test_rule_2")) 165 | assert.False(t, client.RuleExists(otherNID, "no_rule")) 166 | assert.False(t, client.RuleExists(otherNID, "test_rule_1")) 167 | 168 | assert.False(t, client.RuleExists("not_a_file", "no_rule")) 169 | 170 | // Stat error 171 | assert.False(t, client.RuleExists("not_a_file", "no_rule")) 172 | } 173 | 174 | func TestClient_WriteRule(t *testing.T) { 175 | client := newTestClient("tenantID", healthyFSClient) 176 | err := client.WriteRule(testNID, sampleRule) 177 | assert.NoError(t, err) 178 | // cannot secure rule 179 | err = client.WriteRule(testNID, badRule) 180 | assert.EqualError(t, err, "error parsing query: 1:11: parse error: unexpected character inside braces: '.'") 181 | // initialize new file 182 | err = client.WriteRule("newPrefix", sampleRule) 183 | assert.NoError(t, err) 184 | // file does not exist 185 | client = newTestClient("tenantID", readErrFSClient) 186 | err = client.WriteRule(testNID, testRule1) 187 | assert.EqualError(t, err, "error reading rules file: read err") 188 | // error writing file 189 | client = newTestClient("tenantID", writeErrFSClient) 190 | err = client.WriteRule(testNID, testRule1) 191 | assert.EqualError(t, err, "error writing rules file: write err") 192 | } 193 | 194 | func TestClient_UpdateRule(t *testing.T) { 195 | client := newTestClient("tenantID", healthyFSClient) 196 | err := client.UpdateRule(testNID, testRule1) 197 | assert.NoError(t, err) 198 | // Returns error when updating non-existent rule 199 | err = client.UpdateRule(testNID, sampleRule) 200 | assert.Error(t, err) 201 | // cannot secure rule 202 | err = client.UpdateRule(testNID, badRule) 203 | assert.EqualError(t, err, "cannot parse expression: \"malformed{.}\", error parsing query: 1:11: parse error: unexpected character inside braces: '.'") 204 | // file does not exist 205 | client = newTestClient("tenantID", readErrFSClient) 206 | err = client.UpdateRule(testNID, testRule1) 207 | assert.EqualError(t, err, "rule file test_rules.yml does not exist: error reading rules file: read err") 208 | // error writing file 209 | client = newTestClient("tenantID", writeErrFSClient) 210 | err = client.UpdateRule(testNID, testRule1) 211 | assert.EqualError(t, err, "error writing rules file: write err") 212 | } 213 | 214 | func TestClient_ReadRules(t *testing.T) { 215 | client := newTestClient("tenantID", healthyFSClient) 216 | 217 | rules, err := client.ReadRules(testNID, "") 218 | assert.NoError(t, err) 219 | assert.Equal(t, 2, len(rules)) 220 | assert.Equal(t, "test_rule_1", rules[0].Alert) 221 | assert.Equal(t, "test_rule_2", rules[1].Alert) 222 | 223 | rules, err = client.ReadRules(otherNID, "") 224 | assert.NoError(t, err) 225 | assert.Equal(t, 2, len(rules)) 226 | assert.Equal(t, "other_rule_1", rules[0].Alert) 227 | assert.Equal(t, "test_rule_2", rules[1].Alert) 228 | 229 | rules, err = client.ReadRules(testNID, "test_rule_1") 230 | assert.NoError(t, err) 231 | assert.Equal(t, 1, len(rules)) 232 | assert.Equal(t, "test_rule_1", rules[0].Alert) 233 | 234 | rules, err = client.ReadRules(testNID, "no_rule") 235 | assert.Error(t, err) 236 | assert.Equal(t, 0, len(rules)) 237 | 238 | // rule file doesn't exist 239 | rules, err = client.ReadRules("not_a_file", "") 240 | assert.NoError(t, err) 241 | assert.Equal(t, rules, []rulefmt.Rule{}) 242 | } 243 | 244 | func TestClient_DeleteRule(t *testing.T) { 245 | client := newTestClient("tenantID", healthyFSClient) 246 | err := client.DeleteRule(testNID, "test_rule_1") 247 | assert.NoError(t, err) 248 | 249 | err = client.DeleteRule(testNID, "no_rule") 250 | assert.Error(t, err) 251 | 252 | // cannot read file 253 | client = newTestClient("tenantID", readErrFSClient) 254 | err = client.DeleteRule(testNID, "test_rule_1") 255 | assert.EqualError(t, err, "error reading rules file: read err") 256 | 257 | // cannot write file 258 | client = newTestClient("tenantID", writeErrFSClient) 259 | err = client.DeleteRule(testNID, "test_rule_1") 260 | assert.EqualError(t, err, "error writing rules file: write err") 261 | } 262 | 263 | func TestClient_BulkUpdateRules(t *testing.T) { 264 | client := newTestClient("tenantID", healthyFSClient) 265 | results, err := client.BulkUpdateRules(testNID, []rulefmt.Rule{sampleRule, testRule1}) 266 | assert.NoError(t, err) 267 | assert.Equal(t, 2, len(results.Statuses)) 268 | assert.Equal(t, 0, len(results.Errors)) 269 | 270 | results, err = client.BulkUpdateRules(testNID, []rulefmt.Rule{badRule, sampleRule, testRule1}) 271 | assert.NoError(t, err) 272 | assert.Equal(t, 2, len(results.Statuses)) 273 | assert.Equal(t, 1, len(results.Errors)) 274 | // Check results string 275 | assert.Equal(t, "Errors: \n\tbad_rule: error parsing query: 1:11: parse error: unexpected character inside braces: '.'\nStatuses: \n\ttestAlert: created\n\ttest_rule_1: updated\n", results.String()) 276 | 277 | // cannot read file 278 | client = newTestClient("tenantID", readErrFSClient) 279 | results, err = client.BulkUpdateRules(testNID, []rulefmt.Rule{sampleRule}) 280 | assert.EqualError(t, err, "error reading rules file: read err") 281 | 282 | // cannot write file 283 | client = newTestClient("tenantID", writeErrFSClient) 284 | results, err = client.BulkUpdateRules(testNID, []rulefmt.Rule{sampleRule}) 285 | assert.EqualError(t, err, "error writing rules file: write err") 286 | } 287 | 288 | func newTestClient(multitenantLabel string, fsClient *mocks.FSClient) alert.PrometheusAlertClient { 289 | dClient := newHealthyDirClient("test") 290 | fileLocks, _ := alert.NewFileLocker(dClient) 291 | tenancy := alert.TenancyConfig{ 292 | RestrictorLabel: multitenantLabel, 293 | RestrictQueries: true, 294 | } 295 | return alert.NewClient(fileLocks, "prometheus-host.com", fsClient, tenancy) 296 | } 297 | 298 | func newFSClient(readFileErr, writeFileErr error) *mocks.FSClient { 299 | fsClient := &mocks.FSClient{} 300 | fsClient.On("Stat", "test_rules.yml").Return(nil, nil) 301 | fsClient.On("Stat", "other_rules.yml").Return(nil, nil) 302 | fsClient.On("Stat", mock.AnythingOfType("string")).Return(nil, errors.New("file not found")) 303 | fsClient.On("ReadFile", "test_rules.yml").Return([]byte(testRuleFile), readFileErr) 304 | fsClient.On("ReadFile", "other_rules.yml").Return([]byte(otherRuleFile), readFileErr) 305 | fsClient.On("ReadFile", mock.AnythingOfType("string")).Return([]byte{}, errors.New("file does not exist")) 306 | fsClient.On("WriteFile", mock.Anything, mock.Anything, mock.Anything).Return(writeFileErr) 307 | fsClient.On("Root").Return("test_rules/") 308 | return fsClient 309 | } 310 | -------------------------------------------------------------------------------- /prometheus/alert/file_locker.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package alert 9 | 10 | import ( 11 | "io/ioutil" 12 | "os" 13 | "sync" 14 | ) 15 | 16 | type FileLocker struct { 17 | fileLocks map[string]*sync.RWMutex 18 | selfMutex sync.Mutex 19 | } 20 | 21 | func NewFileLocker(fs DirectoryClient) (*FileLocker, error) { 22 | fileLocks := make(map[string]*sync.RWMutex) 23 | 24 | _, err := fs.Stat() 25 | if err != nil { 26 | _ = fs.Mkdir(0766) 27 | } 28 | 29 | files, err := fs.ReadDir() 30 | if err != nil { 31 | return nil, err 32 | } 33 | for _, f := range files { 34 | fullPath := fs.Dir() + "/" + f.Name() 35 | fileLocks[fullPath] = &sync.RWMutex{} 36 | } 37 | return &FileLocker{ 38 | fileLocks: fileLocks, 39 | }, nil 40 | } 41 | 42 | // Lock locks the mutex associated with the given filename for writing. If 43 | // mutex does not exist in map yet, create one. 44 | func (f *FileLocker) Lock(filename string) { 45 | mtx, ok := f.fileLocks[filename] 46 | if !ok { 47 | f.selfMutex.Lock() 48 | defer f.selfMutex.Unlock() 49 | mtx, ok := f.fileLocks[filename] 50 | if !ok { 51 | f.fileLocks[filename] = &sync.RWMutex{} 52 | f.fileLocks[filename].Lock() 53 | return 54 | } 55 | mtx.Lock() 56 | return 57 | } 58 | mtx.Lock() 59 | } 60 | 61 | // Unlock unlocks the mutex associated with the given filename for writing. 62 | // No-op if mutex does not exist in map 63 | func (f *FileLocker) Unlock(filename string) { 64 | if mutex, ok := f.fileLocks[filename]; ok { 65 | mutex.Unlock() 66 | } 67 | } 68 | 69 | // RLock locks the mutex associated with the given filename for reading. 70 | // No-op if mutex does not exist in map 71 | func (f *FileLocker) RLock(filename string) { 72 | mtx, ok := f.fileLocks[filename] 73 | if !ok { 74 | f.selfMutex.Lock() 75 | defer f.selfMutex.Unlock() 76 | mtx, ok := f.fileLocks[filename] 77 | if !ok { 78 | f.fileLocks[filename] = &sync.RWMutex{} 79 | f.fileLocks[filename].RLock() 80 | return 81 | } 82 | mtx.RLock() 83 | return 84 | } 85 | mtx.RLock() 86 | } 87 | 88 | // RUnlock unlocks the mutex associated with the given filename for reading. 89 | // No-op if mutex does not exist in map 90 | func (f *FileLocker) RUnlock(filename string) { 91 | if mutex, ok := f.fileLocks[filename]; ok { 92 | mutex.RUnlock() 93 | } 94 | } 95 | 96 | // DirectoryClient provides the necessary functions to read and modify a single 97 | // directory for the FileLocker to operate 98 | type DirectoryClient interface { 99 | Stat() (os.FileInfo, error) 100 | Mkdir(perm os.FileMode) error 101 | ReadDir() ([]os.FileInfo, error) 102 | Dir() string 103 | } 104 | 105 | type dirClient struct { 106 | rulesDir string 107 | } 108 | 109 | func (f *dirClient) Stat() (os.FileInfo, error) { 110 | return os.Stat(f.rulesDir) 111 | } 112 | 113 | func (f *dirClient) Mkdir(perm os.FileMode) error { 114 | return os.Mkdir(f.rulesDir, perm) 115 | } 116 | 117 | func (f *dirClient) ReadDir() ([]os.FileInfo, error) { 118 | return ioutil.ReadDir(f.rulesDir) 119 | } 120 | 121 | func (f *dirClient) Dir() string { 122 | return f.rulesDir 123 | } 124 | 125 | func NewDirectoryClient(rulesDir string) DirectoryClient { 126 | return &dirClient{ 127 | rulesDir: rulesDir, 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /prometheus/alert/file_locker_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package alert_test 9 | 10 | import ( 11 | "testing" 12 | "time" 13 | 14 | "github.com/facebookincubator/prometheus-configmanager/prometheus/alert" 15 | "github.com/facebookincubator/prometheus-configmanager/prometheus/alert/mocks" 16 | 17 | "github.com/stretchr/testify/assert" 18 | "github.com/stretchr/testify/mock" 19 | ) 20 | 21 | func TestFileLocker_Lock(t *testing.T) { 22 | locks, err := alert.NewFileLocker(newHealthyDirClient("test")) 23 | assert.NoError(t, err) 24 | fname := "file1" 25 | var events []int 26 | 27 | locks.Lock(fname) 28 | events = append(events, 1) 29 | go func() { 30 | locks.Lock(fname) 31 | events = append(events, 2) 32 | }() 33 | events = append(events, 3) 34 | 35 | // Assert event 2 has not happened yet since it is locked 36 | assert.Equal(t, []int{1, 3}, events) 37 | } 38 | 39 | func TestFileLocker_Unlock(t *testing.T) { 40 | locks, err := alert.NewFileLocker(newHealthyDirClient("test")) 41 | assert.NoError(t, err) 42 | fname := "file1" 43 | var events []int 44 | 45 | locks.Lock(fname) 46 | events = append(events, 1) 47 | go func() { 48 | locks.Lock(fname) 49 | events = append(events, 2) 50 | }() 51 | events = append(events, 3) 52 | locks.Unlock(fname) 53 | 54 | // sleep to let go func finish 55 | time.Sleep(500 * time.Millisecond) 56 | // Assert event 2 happened after event 3 57 | assert.Equal(t, []int{1, 3, 2}, events) 58 | } 59 | 60 | func TestFileLocker_RLock(t *testing.T) { 61 | locks, err := alert.NewFileLocker(newHealthyDirClient("test")) 62 | assert.NoError(t, err) 63 | fname := "file1" 64 | var events []int 65 | 66 | locks.RLock(fname) 67 | events = append(events, 1) 68 | go func() { 69 | locks.RLock(fname) 70 | events = append(events, 2) 71 | }() 72 | time.Sleep(500 * time.Millisecond) 73 | events = append(events, 3) 74 | go func() { 75 | locks.Lock(fname) 76 | events = append(events, 4) 77 | }() 78 | // Fname can be RLocked multiple times so events are in order, but cannot 79 | // be Locked so event 4 did not happen 80 | assert.Equal(t, []int{1, 2, 3}, events) 81 | } 82 | 83 | func TestFileLocker_RUnlock(t *testing.T) { 84 | locks, err := alert.NewFileLocker(newHealthyDirClient("test")) 85 | assert.NoError(t, err) 86 | fname := "file1" 87 | var events []int 88 | 89 | locks.RLock(fname) 90 | events = append(events, 1) 91 | go func() { 92 | locks.RLock(fname) 93 | events = append(events, 2) 94 | locks.RUnlock(fname) 95 | }() 96 | time.Sleep(500 * time.Millisecond) 97 | 98 | go func() { 99 | locks.Lock(fname) 100 | events = append(events, 3) 101 | }() 102 | 103 | events = append(events, 4) 104 | locks.RUnlock(fname) 105 | time.Sleep(500 * time.Millisecond) 106 | 107 | // Assert event 3 happened after 4 108 | assert.Equal(t, []int{1, 2, 4, 3}, events) 109 | } 110 | 111 | // creates mock directory client that doesn't return errors 112 | func newHealthyDirClient(rulesDir string) *mocks.DirectoryClient { 113 | client := &mocks.DirectoryClient{} 114 | client.On("Stat", mock.AnythingOfType("string")).Return(nil, nil) 115 | client.On("Mkdir", mock.AnythingOfType("os.FileMode")).Return(nil) 116 | client.On("ReadDir").Return(nil, nil) 117 | client.On("Dir").Return(rulesDir) 118 | 119 | return client 120 | } 121 | -------------------------------------------------------------------------------- /prometheus/alert/mocks/DirectoryClient.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | // Code generated by mockery v1.0.0. DO NOT EDIT. 9 | 10 | package mocks 11 | 12 | import ( 13 | os "os" 14 | 15 | mock "github.com/stretchr/testify/mock" 16 | ) 17 | 18 | // DirectoryClient is an autogenerated mock type for the DirectoryClient type 19 | type DirectoryClient struct { 20 | mock.Mock 21 | } 22 | 23 | // Dir provides a mock function with given fields: 24 | func (_m *DirectoryClient) Dir() string { 25 | ret := _m.Called() 26 | 27 | var r0 string 28 | if rf, ok := ret.Get(0).(func() string); ok { 29 | r0 = rf() 30 | } else { 31 | r0 = ret.Get(0).(string) 32 | } 33 | 34 | return r0 35 | } 36 | 37 | // Mkdir provides a mock function with given fields: perm 38 | func (_m *DirectoryClient) Mkdir(perm os.FileMode) error { 39 | ret := _m.Called(perm) 40 | 41 | var r0 error 42 | if rf, ok := ret.Get(0).(func(os.FileMode) error); ok { 43 | r0 = rf(perm) 44 | } else { 45 | r0 = ret.Error(0) 46 | } 47 | 48 | return r0 49 | } 50 | 51 | // ReadDir provides a mock function with given fields: 52 | func (_m *DirectoryClient) ReadDir() ([]os.FileInfo, error) { 53 | ret := _m.Called() 54 | 55 | var r0 []os.FileInfo 56 | if rf, ok := ret.Get(0).(func() []os.FileInfo); ok { 57 | r0 = rf() 58 | } else { 59 | if ret.Get(0) != nil { 60 | r0 = ret.Get(0).([]os.FileInfo) 61 | } 62 | } 63 | 64 | var r1 error 65 | if rf, ok := ret.Get(1).(func() error); ok { 66 | r1 = rf() 67 | } else { 68 | r1 = ret.Error(1) 69 | } 70 | 71 | return r0, r1 72 | } 73 | 74 | // Stat provides a mock function with given fields: 75 | func (_m *DirectoryClient) Stat() (os.FileInfo, error) { 76 | ret := _m.Called() 77 | 78 | var r0 os.FileInfo 79 | if rf, ok := ret.Get(0).(func() os.FileInfo); ok { 80 | r0 = rf() 81 | } else { 82 | if ret.Get(0) != nil { 83 | r0 = ret.Get(0).(os.FileInfo) 84 | } 85 | } 86 | 87 | var r1 error 88 | if rf, ok := ret.Get(1).(func() error); ok { 89 | r1 = rf() 90 | } else { 91 | r1 = ret.Error(1) 92 | } 93 | 94 | return r0, r1 95 | } 96 | -------------------------------------------------------------------------------- /prometheus/alert/mocks/PrometheusAlertClient.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.0.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | alert "github.com/facebookincubator/prometheus-configmanager/prometheus/alert" 7 | mock "github.com/stretchr/testify/mock" 8 | 9 | rulefmt "github.com/prometheus/prometheus/pkg/rulefmt" 10 | ) 11 | 12 | // PrometheusAlertClient is an autogenerated mock type for the PrometheusAlertClient type 13 | type PrometheusAlertClient struct { 14 | mock.Mock 15 | } 16 | 17 | // BulkUpdateRules provides a mock function with given fields: filePrefix, rules 18 | func (_m *PrometheusAlertClient) BulkUpdateRules(filePrefix string, rules []rulefmt.Rule) (alert.BulkUpdateResults, error) { 19 | ret := _m.Called(filePrefix, rules) 20 | 21 | var r0 alert.BulkUpdateResults 22 | if rf, ok := ret.Get(0).(func(string, []rulefmt.Rule) alert.BulkUpdateResults); ok { 23 | r0 = rf(filePrefix, rules) 24 | } else { 25 | r0 = ret.Get(0).(alert.BulkUpdateResults) 26 | } 27 | 28 | var r1 error 29 | if rf, ok := ret.Get(1).(func(string, []rulefmt.Rule) error); ok { 30 | r1 = rf(filePrefix, rules) 31 | } else { 32 | r1 = ret.Error(1) 33 | } 34 | 35 | return r0, r1 36 | } 37 | 38 | // DeleteRule provides a mock function with given fields: filePrefix, ruleName 39 | func (_m *PrometheusAlertClient) DeleteRule(filePrefix string, ruleName string) error { 40 | ret := _m.Called(filePrefix, ruleName) 41 | 42 | var r0 error 43 | if rf, ok := ret.Get(0).(func(string, string) error); ok { 44 | r0 = rf(filePrefix, ruleName) 45 | } else { 46 | r0 = ret.Error(0) 47 | } 48 | 49 | return r0 50 | } 51 | 52 | // ReadRules provides a mock function with given fields: filePrefix, ruleName 53 | func (_m *PrometheusAlertClient) ReadRules(filePrefix string, ruleName string) ([]rulefmt.Rule, error) { 54 | ret := _m.Called(filePrefix, ruleName) 55 | 56 | var r0 []rulefmt.Rule 57 | if rf, ok := ret.Get(0).(func(string, string) []rulefmt.Rule); ok { 58 | r0 = rf(filePrefix, ruleName) 59 | } else { 60 | if ret.Get(0) != nil { 61 | r0 = ret.Get(0).([]rulefmt.Rule) 62 | } 63 | } 64 | 65 | var r1 error 66 | if rf, ok := ret.Get(1).(func(string, string) error); ok { 67 | r1 = rf(filePrefix, ruleName) 68 | } else { 69 | r1 = ret.Error(1) 70 | } 71 | 72 | return r0, r1 73 | } 74 | 75 | // ReloadPrometheus provides a mock function with given fields: 76 | func (_m *PrometheusAlertClient) ReloadPrometheus() error { 77 | ret := _m.Called() 78 | 79 | var r0 error 80 | if rf, ok := ret.Get(0).(func() error); ok { 81 | r0 = rf() 82 | } else { 83 | r0 = ret.Error(0) 84 | } 85 | 86 | return r0 87 | } 88 | 89 | // RuleExists provides a mock function with given fields: filePrefix, rulename 90 | func (_m *PrometheusAlertClient) RuleExists(filePrefix string, rulename string) bool { 91 | ret := _m.Called(filePrefix, rulename) 92 | 93 | var r0 bool 94 | if rf, ok := ret.Get(0).(func(string, string) bool); ok { 95 | r0 = rf(filePrefix, rulename) 96 | } else { 97 | r0 = ret.Get(0).(bool) 98 | } 99 | 100 | return r0 101 | } 102 | 103 | // Tenancy provides a mock function with given fields: 104 | func (_m *PrometheusAlertClient) Tenancy() alert.TenancyConfig { 105 | ret := _m.Called() 106 | 107 | var r0 alert.TenancyConfig 108 | if rf, ok := ret.Get(0).(func() alert.TenancyConfig); ok { 109 | r0 = rf() 110 | } else { 111 | r0 = ret.Get(0).(alert.TenancyConfig) 112 | } 113 | 114 | return r0 115 | } 116 | 117 | // UpdateRule provides a mock function with given fields: filePrefix, rule 118 | func (_m *PrometheusAlertClient) UpdateRule(filePrefix string, rule rulefmt.Rule) error { 119 | ret := _m.Called(filePrefix, rule) 120 | 121 | var r0 error 122 | if rf, ok := ret.Get(0).(func(string, rulefmt.Rule) error); ok { 123 | r0 = rf(filePrefix, rule) 124 | } else { 125 | r0 = ret.Error(0) 126 | } 127 | 128 | return r0 129 | } 130 | 131 | // WriteRule provides a mock function with given fields: filePrefix, rule 132 | func (_m *PrometheusAlertClient) WriteRule(filePrefix string, rule rulefmt.Rule) error { 133 | ret := _m.Called(filePrefix, rule) 134 | 135 | var r0 error 136 | if rf, ok := ret.Get(0).(func(string, rulefmt.Rule) error); ok { 137 | r0 = rf(filePrefix, rule) 138 | } else { 139 | r0 = ret.Error(0) 140 | } 141 | 142 | return r0 143 | } 144 | -------------------------------------------------------------------------------- /prometheus/docs/swagger-v1.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # This source code is licensed under the MIT license found in the 3 | # LICENSE file in the root directory of this source tree. 4 | 5 | --- 6 | swagger: '2.0' 7 | info: 8 | title: Prometheus Configurer Model Definitions and Paths 9 | description: Prometheus Configurer REST APIs 10 | version: 1.0.0 11 | 12 | paths: 13 | /{tenant_id}/alert: 14 | get: 15 | summary: Retrieve alerting rule configurations 16 | parameters: 17 | - $ref: '#/parameters/tenant_id' 18 | - in: query 19 | name: alert_name 20 | type: string 21 | description: Optional name of alert to retrieve 22 | required: false 23 | responses: 24 | '200': 25 | description: 26 | List of alert configurations 27 | schema: 28 | type: array 29 | items: 30 | $ref: '#/definitions/alert_config' 31 | default: 32 | $ref: '#/responses/UnexpectedError' 33 | post: 34 | summary: Configure alerting rule 35 | parameters: 36 | - $ref: '#/parameters/tenant_id' 37 | - in: body 38 | name: alert_config 39 | description: Alerting rule that is to be added 40 | required: true 41 | schema: 42 | $ref: '#/definitions/alert_config' 43 | responses: 44 | '201': 45 | description: Created 46 | default: 47 | $ref: '#/responses/UnexpectedError' 48 | 49 | /{tenant_id}/alert/{alert_name}: 50 | get: 51 | summary: Retrieve an alerting rule 52 | parameters: 53 | - $ref: '#/parameters/tenant_id' 54 | - in: path 55 | name: alert_name 56 | description: Name of alert to be retrieved 57 | required: true 58 | type: string 59 | responses: 60 | '200': 61 | description: Alert configuration 62 | schema: 63 | $ref: '#/definitions/alert_config' 64 | default: 65 | $ref: '#/responses/UnexpectedError' 66 | delete: 67 | summary: Delete an alerting rule 68 | parameters: 69 | - $ref: '#/parameters/tenant_id' 70 | - in: path 71 | name: alert_name 72 | description: Name of alert to be deleted 73 | required: true 74 | type: string 75 | responses: 76 | '204': 77 | description: Deleted 78 | default: 79 | $ref: '#/responses/UnexpectedError' 80 | put: 81 | summary: Update an existing alerting rule 82 | parameters: 83 | - $ref: '#/parameters/tenant_id' 84 | - in: path 85 | name: alert_name 86 | description: Name of alert to be updated 87 | required: true 88 | type: string 89 | - in: body 90 | name: alert_config 91 | description: Updated alerting rule 92 | required: true 93 | schema: 94 | $ref: '#/definitions/alert_config' 95 | responses: 96 | '204': 97 | description: Updated 98 | default: 99 | $ref: '#/responses/UnexpectedError' 100 | 101 | /{tenant_id}/alert/bulk: 102 | post: 103 | summary: Bulk update/create alerting rules 104 | parameters: 105 | - $ref: '#/parameters/tenant_id' 106 | - in: body 107 | name: alert_configs 108 | description: Alerting rules to be updated or created 109 | required: true 110 | schema: 111 | $ref: '#/definitions/alert_config_list' 112 | responses: 113 | '200': 114 | description: Success 115 | schema: 116 | $ref: '#/definitions/alert_bulk_upload_response' 117 | default: 118 | $ref: '#/responses/UnexpectedError' 119 | 120 | /tenancy: 121 | get: 122 | summary: Retrieve tenancy configuration of configurer service 123 | responses: 124 | '200': 125 | description: Tenancy configuration 126 | schema: 127 | $ref: '#/definitions/tenancy_config': 128 | 129 | 130 | parameters: 131 | tenant_id: 132 | description: Tenant ID 133 | in: query 134 | name: tenant_id 135 | required: false 136 | type: string 137 | 138 | definitions: 139 | alert_config: 140 | type: object 141 | required: 142 | - alert 143 | - expr 144 | properties: 145 | alert: 146 | type: string 147 | expr: 148 | type: string 149 | labels: 150 | $ref: '#/definitions/alert_labels' 151 | for: 152 | type: string 153 | annotations: 154 | $ref: '#/definitions/alert_labels' 155 | 156 | alert_config_list: 157 | type: array 158 | items: 159 | $ref: '#/definitions/alert_config' 160 | 161 | alert_bulk_upload_response: 162 | type: object 163 | required: 164 | - errors 165 | - statuses 166 | properties: 167 | errors: 168 | type: object 169 | additionalProperties: 170 | type: string 171 | statuses: 172 | type: object 173 | additionalProperties: 174 | type: string 175 | 176 | alert_labels: 177 | type: object 178 | additionalProperties: 179 | type: string 180 | 181 | tenancy_config: 182 | type: object 183 | properties: 184 | restrictor_label: 185 | type: string 186 | restrict_queries: 187 | type: boolean 188 | 189 | error: 190 | type: object 191 | required: 192 | - message 193 | properties: 194 | message: 195 | example: Error string 196 | type: string 197 | 198 | responses: 199 | UnexpectedError: 200 | description: Unexpected Error 201 | schema: 202 | $ref: '#/definitions/error' 203 | -------------------------------------------------------------------------------- /prometheus/docs/swagger.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # This source code is licensed under the MIT license found in the 3 | # LICENSE file in the root directory of this source tree. 4 | 5 | --- 6 | swagger: '2.0' 7 | info: 8 | title: Prometheus Configurer Model Definitions and Paths 9 | description: Prometheus Configurer REST APIs 10 | version: 0.1.0 11 | 12 | paths: 13 | /{tenant_id}/alert: 14 | get: 15 | summary: Retrieve alerting rule configurations 16 | parameters: 17 | - $ref: '#/parameters/tenant_id' 18 | - in: query 19 | name: alert_name 20 | type: string 21 | description: Optional name of alert to retrieve 22 | required: false 23 | responses: 24 | '200': 25 | description: 26 | List of alert configurations 27 | schema: 28 | type: array 29 | items: 30 | $ref: '#/definitions/alert_config' 31 | default: 32 | $ref: '#/responses/UnexpectedError' 33 | post: 34 | summary: Configure alerting rule 35 | parameters: 36 | - $ref: '#/parameters/tenant_id' 37 | - in: body 38 | name: alert_config 39 | description: Alerting rule that is to be added 40 | required: true 41 | schema: 42 | $ref: '#/definitions/alert_config' 43 | responses: 44 | '201': 45 | description: Created 46 | default: 47 | $ref: '#/responses/UnexpectedError' 48 | delete: 49 | summary: Delete an alerting rule 50 | parameters: 51 | - $ref: '#/parameters/tenant_id' 52 | - in: query 53 | name: alert_name 54 | description: Name of alert to be deleted 55 | required: true 56 | type: string 57 | responses: 58 | '200': 59 | description: Deleted 60 | default: 61 | $ref: '#/responses/UnexpectedError' 62 | 63 | /{tenant_id}/alert/{alert_name}: 64 | put: 65 | summary: Update an existing alerting rule 66 | parameters: 67 | - $ref: '#/parameters/tenant_id' 68 | - in: path 69 | name: alert_name 70 | description: Name of alert to be updated 71 | required: true 72 | type: string 73 | - in: body 74 | name: alert_config 75 | description: Updated alerting rule 76 | required: true 77 | schema: 78 | $ref: '#/definitions/alert_config' 79 | responses: 80 | '200': 81 | description: Updated 82 | default: 83 | $ref: '#/responses/UnexpectedError' 84 | 85 | /{tenant_id}/alert/bulk: 86 | put: 87 | summary: Bulk update/create alerting rules 88 | parameters: 89 | - $ref: '#/parameters/tenant_id' 90 | - in: body 91 | name: alert_configs 92 | description: Alerting rules to be updated or created 93 | required: true 94 | schema: 95 | $ref: '#/definitions/alert_config_list' 96 | responses: 97 | '200': 98 | description: Success 99 | schema: 100 | $ref: '#/definitions/alert_bulk_upload_response' 101 | default: 102 | $ref: '#/responses/UnexpectedError' 103 | 104 | parameters: 105 | tenant_id: 106 | description: Tenant ID 107 | in: path 108 | name: tenant_id 109 | required: true 110 | type: string 111 | 112 | definitions: 113 | alert_config: 114 | type: object 115 | required: 116 | - alert 117 | - expr 118 | properties: 119 | alert: 120 | type: string 121 | expr: 122 | type: string 123 | labels: 124 | $ref: '#/definitions/alert_labels' 125 | for: 126 | type: string 127 | annotations: 128 | $ref: '#/definitions/alert_labels' 129 | 130 | alert_config_list: 131 | type: array 132 | items: 133 | $ref: '#/definitions/alert_config' 134 | 135 | alert_bulk_upload_response: 136 | type: object 137 | required: 138 | - errors 139 | - statuses 140 | properties: 141 | errors: 142 | type: object 143 | additionalProperties: 144 | type: string 145 | statuses: 146 | type: object 147 | additionalProperties: 148 | type: string 149 | 150 | alert_labels: 151 | type: object 152 | additionalProperties: 153 | type: string 154 | 155 | error: 156 | type: object 157 | required: 158 | - message 159 | properties: 160 | message: 161 | example: Error string 162 | type: string 163 | 164 | responses: 165 | UnexpectedError: 166 | description: Unexpected Error 167 | schema: 168 | $ref: '#/definitions/error' 169 | -------------------------------------------------------------------------------- /prometheus/handlers/handlers.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package handlers 9 | 10 | import ( 11 | "encoding/json" 12 | "fmt" 13 | "io/ioutil" 14 | "net/http" 15 | 16 | "github.com/facebookincubator/prometheus-configmanager/prometheus/alert" 17 | "github.com/golang/glog" 18 | "github.com/labstack/echo" 19 | "github.com/prometheus/prometheus/pkg/rulefmt" 20 | ) 21 | 22 | const ( 23 | v0rootPath = "/:tenant_id" 24 | v0alertPath = "/alert" 25 | v0alertUpdatePath = v0alertPath + "/:" + ruleNameParam 26 | v0alertBulkPath = v0alertPath + "/bulk" 27 | 28 | ruleNameParam = "alert_name" 29 | 30 | tenantIDParam = "tenant_id" 31 | 32 | v1rootPath = "/v1" 33 | v1TenantRootPath = v1rootPath + "/:tenant_id" 34 | 35 | v1alertPath = "/alert" 36 | v1alertBulkPath = v1alertPath + "/bulk" 37 | v1alertNamePath = v1alertPath + "/:" + ruleNameParam 38 | v1TenancyPath = "/tenancy" 39 | ) 40 | 41 | func statusHandler(c echo.Context) error { 42 | return c.String(http.StatusOK, "Prometheus Config server") 43 | } 44 | 45 | func RegisterBaseHandlers(e *echo.Echo) { 46 | e.GET("/", statusHandler) 47 | } 48 | 49 | func RegisterV0Handlers(e *echo.Echo, alertClient alert.PrometheusAlertClient) { 50 | v0 := e.Group(v0rootPath) 51 | v0.Use(tenancyMiddlewareProvider(pathTenantProvider)) 52 | 53 | v0.POST(v0alertPath, GetConfigureAlertHandler(alertClient)) 54 | v0.GET(v0alertPath, GetRetrieveAlertHandler(alertClient)) 55 | v0.DELETE(v0alertPath, GetDeleteAlertHandler(alertClient, queryAlertNameProvider)) 56 | 57 | v0.PUT(v0alertUpdatePath, GetUpdateAlertHandler(alertClient)) 58 | 59 | v0.PUT(v0alertBulkPath, GetBulkAlertUpdateHandler(alertClient)) 60 | } 61 | 62 | func RegisterV1Handlers(e *echo.Echo, alertClient alert.PrometheusAlertClient) { 63 | v1 := e.Group(v1rootPath) 64 | 65 | v1.GET(v1TenancyPath, GetGetTenancyHandler(alertClient)) 66 | 67 | v1Tenant := e.Group(v1TenantRootPath) 68 | v1Tenant.Use(tenancyMiddlewareProvider(pathTenantProvider)) 69 | 70 | v1Tenant.POST(v1alertPath, GetConfigureAlertHandler(alertClient)) 71 | v1Tenant.GET(v1alertPath, GetRetrieveAlertHandler(alertClient)) 72 | 73 | v1Tenant.DELETE(v1alertNamePath, GetDeleteAlertHandler(alertClient, pathAlertNameProvider)) 74 | v1Tenant.PUT(v1alertNamePath, GetUpdateAlertHandler(alertClient)) 75 | v1Tenant.GET(v1alertNamePath, GetRetrieveAlertHandler(alertClient)) 76 | 77 | v1Tenant.POST(v1alertBulkPath, GetBulkAlertUpdateHandler(alertClient)) 78 | } 79 | 80 | // Returns middleware func to check for tenant_id 81 | func tenancyMiddlewareProvider(getTenantID paramProvider) echo.MiddlewareFunc { 82 | return func(next echo.HandlerFunc) echo.HandlerFunc { 83 | return func(c echo.Context) error { 84 | providedTenantID := getTenantID(c) 85 | if providedTenantID == "" { 86 | return echo.NewHTTPError(http.StatusBadRequest, "Must provide tenant_id parameter") 87 | } 88 | c.Set(tenantIDParam, providedTenantID) 89 | return next(c) 90 | } 91 | } 92 | } 93 | 94 | type paramProvider func(c echo.Context) string 95 | 96 | // V0 tenantID is a path parameter 97 | var pathTenantProvider = func(c echo.Context) string { 98 | return c.Param(tenantIDParam) 99 | } 100 | 101 | var pathAlertNameProvider = func(c echo.Context) string { 102 | return c.Param(ruleNameParam) 103 | } 104 | 105 | var queryAlertNameProvider = func(c echo.Context) string { 106 | return c.QueryParam(ruleNameParam) 107 | } 108 | 109 | // GetConfigureAlertHandler returns a handler that calls the client method WriteAlert() to 110 | // write the alert configuration from the body of this request 111 | func GetConfigureAlertHandler(client alert.PrometheusAlertClient) func(c echo.Context) error { 112 | return func(c echo.Context) error { 113 | defer glog.Flush() 114 | rule, err := decodeRulePostRequest(c) 115 | if err != nil { 116 | return echo.NewHTTPError(http.StatusBadRequest, err.Error()) 117 | } 118 | tenantID := c.Get(tenantIDParam).(string) 119 | glog.Infof("Configure Alert: Tenant: %s, %+v", tenantID, rule) 120 | 121 | err = alert.ValidateRule(rule) 122 | if err != nil { 123 | return echo.NewHTTPError(http.StatusBadRequest, err.Error()) 124 | } 125 | 126 | if client.RuleExists(tenantID, rule.Alert) { 127 | return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Rule '%s' already exists", rule.Alert)) 128 | } 129 | 130 | err = client.WriteRule(tenantID, rule) 131 | if err != nil { 132 | return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) 133 | } 134 | 135 | err = client.ReloadPrometheus() 136 | if err != nil { 137 | return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) 138 | } 139 | return c.NoContent(http.StatusOK) 140 | } 141 | } 142 | 143 | func GetRetrieveAlertHandler(client alert.PrometheusAlertClient) func(c echo.Context) error { 144 | return func(c echo.Context) error { 145 | defer glog.Flush() 146 | ruleName := c.QueryParam(ruleNameParam) 147 | tenantID := c.Get(tenantIDParam).(string) 148 | glog.Infof("Get Rule: Tenant: %s, rule: %s", tenantID, ruleName) 149 | 150 | rules, err := client.ReadRules(tenantID, ruleName) 151 | if err != nil { 152 | return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) 153 | } 154 | return c.JSON(http.StatusOK, rulesToJSON(rules)) 155 | } 156 | } 157 | 158 | func GetDeleteAlertHandler(client alert.PrometheusAlertClient, getRuleName paramProvider) func(c echo.Context) error { 159 | return func(c echo.Context) error { 160 | defer glog.Flush() 161 | ruleName := getRuleName(c) 162 | tenantID := c.Get(tenantIDParam).(string) 163 | glog.Infof("Delete Rule: Tenant: %s, rule: %+v", tenantID, ruleName) 164 | 165 | if ruleName == "" { 166 | return echo.NewHTTPError(http.StatusBadRequest, "No rule name provided") 167 | } 168 | err := client.DeleteRule(tenantID, ruleName) 169 | if err != nil { 170 | return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) 171 | } 172 | err = client.ReloadPrometheus() 173 | if err != nil { 174 | return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) 175 | } 176 | return c.String(http.StatusNoContent, fmt.Sprintf("rule %s deleted", ruleName)) 177 | } 178 | } 179 | 180 | func GetUpdateAlertHandler(client alert.PrometheusAlertClient) func(c echo.Context) error { 181 | return func(c echo.Context) error { 182 | defer glog.Flush() 183 | ruleName := c.Param(ruleNameParam) 184 | tenantID := c.Get(tenantIDParam).(string) 185 | glog.Infof("Update Rule: Tenant: %s, rule: %s", tenantID, ruleName) 186 | 187 | if ruleName == "" { 188 | return echo.NewHTTPError(http.StatusBadRequest, "No rule name provided") 189 | } 190 | 191 | if !client.RuleExists(tenantID, ruleName) { 192 | return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Rule '%s' does not exist", ruleName)) 193 | } 194 | 195 | rule, err := decodeRulePostRequest(c) 196 | if err != nil { 197 | return echo.NewHTTPError(http.StatusBadRequest, err.Error()) 198 | } 199 | 200 | err = alert.ValidateRule(rule) 201 | if err != nil { 202 | return echo.NewHTTPError(http.StatusBadRequest, err.Error()) 203 | } 204 | 205 | err = client.UpdateRule(tenantID, rule) 206 | if err != nil { 207 | return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) 208 | } 209 | 210 | err = client.ReloadPrometheus() 211 | if err != nil { 212 | return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) 213 | } 214 | return c.NoContent(http.StatusNoContent) 215 | } 216 | } 217 | 218 | func GetBulkAlertUpdateHandler(client alert.PrometheusAlertClient) func(c echo.Context) error { 219 | return func(c echo.Context) error { 220 | defer glog.Flush() 221 | tenantID := c.Get(tenantIDParam).(string) 222 | rules, err := decodeBulkRulesPostRequest(c) 223 | if err != nil { 224 | return echo.NewHTTPError(http.StatusBadRequest, err.Error()) 225 | } 226 | glog.Infof("Bulk Update Rules: Tenant: %s, rules: %d", tenantID, len(rules)) 227 | 228 | for _, rule := range rules { 229 | err = alert.ValidateRule(rule) 230 | if err != nil { 231 | return echo.NewHTTPError(http.StatusBadRequest, err.Error()) 232 | } 233 | } 234 | 235 | results, err := client.BulkUpdateRules(tenantID, rules) 236 | if err != nil { 237 | return echo.NewHTTPError(http.StatusBadRequest, err.Error()) 238 | } 239 | 240 | err = client.ReloadPrometheus() 241 | if err != nil { 242 | return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) 243 | } 244 | return c.JSON(http.StatusOK, results) 245 | } 246 | } 247 | 248 | func GetGetTenancyHandler(client alert.PrometheusAlertClient) func(c echo.Context) error { 249 | return func(c echo.Context) error { 250 | return c.JSON(http.StatusOK, client.Tenancy()) 251 | } 252 | } 253 | 254 | func decodeRulePostRequest(c echo.Context) (rulefmt.Rule, error) { 255 | body, err := ioutil.ReadAll(c.Request().Body) 256 | if err != nil { 257 | glog.Errorf("Error reading rule payload: %v", err) 258 | return rulefmt.Rule{}, fmt.Errorf("error reading request body: %v", err) 259 | } 260 | // First try unmarshaling into prometheus rulefmt.Rule{} 261 | payload := rulefmt.Rule{} 262 | err = json.Unmarshal(body, &payload) 263 | if err == nil { 264 | return payload, nil 265 | } 266 | // Try to unmarshal into the RuleJSONWrapper struct if prometheus struct doesn't work 267 | jsonPayload := alert.RuleJSONWrapper{} 268 | err = json.Unmarshal(body, &jsonPayload) 269 | if err != nil { 270 | glog.Errorf("Error unmarshaling rule payload: %v", err) 271 | return payload, fmt.Errorf("error unmarshalling payload: %v", err) 272 | } 273 | return jsonPayload.ToRuleFmt() 274 | } 275 | 276 | func decodeBulkRulesPostRequest(c echo.Context) ([]rulefmt.Rule, error) { 277 | body, err := ioutil.ReadAll(c.Request().Body) 278 | if err != nil { 279 | glog.Errorf("Error reading bulk rules payload: %v", err) 280 | return []rulefmt.Rule{}, fmt.Errorf("error reading request body: %v", err) 281 | } 282 | var payload []rulefmt.Rule 283 | err = json.Unmarshal(body, &payload) 284 | if err == nil { 285 | return payload, nil 286 | } 287 | // Try to unmarshal into the RuleJSONWrapper struct if prometheus struct doesn't work 288 | jsonPayload := []alert.RuleJSONWrapper{} 289 | err = json.Unmarshal(body, &jsonPayload) 290 | if err != nil { 291 | glog.Errorf("Error unmarshaling bulk rules: %v", err) 292 | return []rulefmt.Rule{}, fmt.Errorf("error unmarshalling payload: %v", err) 293 | } 294 | return rulesFromJSON(jsonPayload) 295 | } 296 | 297 | func rulesToJSON(rules []rulefmt.Rule) []alert.RuleJSONWrapper { 298 | ret := make([]alert.RuleJSONWrapper, 0) 299 | for _, rule := range rules { 300 | ret = append(ret, *rulefmtToJSON(rule)) 301 | } 302 | return ret 303 | } 304 | 305 | func rulesFromJSON(rules []alert.RuleJSONWrapper) ([]rulefmt.Rule, error) { 306 | ret := make([]rulefmt.Rule, 0) 307 | for _, rule := range rules { 308 | jsonRule, err := rule.ToRuleFmt() 309 | if err != nil { 310 | return ret, err 311 | } 312 | ret = append(ret, jsonRule) 313 | } 314 | return ret, nil 315 | } 316 | 317 | func rulefmtToJSON(rule rulefmt.Rule) *alert.RuleJSONWrapper { 318 | return &alert.RuleJSONWrapper{ 319 | Record: rule.Record, 320 | Alert: rule.Alert, 321 | Expr: rule.Expr, 322 | For: rule.For.String(), 323 | Labels: rule.Labels, 324 | Annotations: rule.Annotations, 325 | } 326 | } 327 | -------------------------------------------------------------------------------- /prometheus/server.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package main 9 | 10 | import ( 11 | "flag" 12 | "fmt" 13 | "io/ioutil" 14 | "os" 15 | "strings" 16 | 17 | "github.com/facebookincubator/prometheus-configmanager/fsclient" 18 | "github.com/facebookincubator/prometheus-configmanager/prometheus/alert" 19 | "github.com/facebookincubator/prometheus-configmanager/prometheus/handlers" 20 | 21 | "github.com/golang/glog" 22 | "github.com/labstack/echo" 23 | "github.com/labstack/echo/middleware" 24 | ) 25 | 26 | const ( 27 | defaultPort = "9100" 28 | defaultPrometheusURL = "prometheus:9090" 29 | defaultTenancyLabel = "tenant" 30 | ) 31 | 32 | func main() { 33 | port := flag.String("port", defaultPort, fmt.Sprintf("Port to listen for requests. Default is %s", defaultPort)) 34 | rulesDir := flag.String("rules-dir", ".", "Directory to write rules files. Default is '.'") 35 | prometheusURL := flag.String("prometheusURL", defaultPrometheusURL, fmt.Sprintf("URL of the prometheus instance that is reading these rules. Default is %s", defaultPrometheusURL)) 36 | multitenancyLabel := flag.String("multitenant-label", "tenant", fmt.Sprintf("The label name to segment alerting rules to enable multi-tenant support, having each tenant's alerts in a separate file. Default is %s", defaultTenancyLabel)) 37 | restrictQueries := flag.Bool("restrict-queries", false, "If this flag is set all alert rule expressions will be restricted to only match series with {=}") 38 | flag.Parse() 39 | 40 | if !strings.HasSuffix(*rulesDir, "/") { 41 | *rulesDir += "/" 42 | } 43 | 44 | // Check if rulesDir exists and create it if not 45 | if _, err := os.Stat(*rulesDir); os.IsNotExist(err) { 46 | files, err := ioutil.ReadDir("/") 47 | if err != nil { 48 | glog.Fatalf("Could not stat directory: %v", err) 49 | } 50 | fmt.Println(files) 51 | err = os.Mkdir(*rulesDir, 0644) 52 | if err != nil { 53 | glog.Fatalf("Could not create rules directory: %v", err) 54 | } 55 | } 56 | 57 | fileLocks, err := alert.NewFileLocker(alert.NewDirectoryClient(*rulesDir)) 58 | clientTenancy := alert.TenancyConfig{ 59 | RestrictQueries: *restrictQueries, 60 | RestrictorLabel: *multitenancyLabel, 61 | } 62 | alertClient := alert.NewClient(fileLocks, *prometheusURL, fsclient.NewFSClient(*rulesDir), clientTenancy) 63 | if err != nil { 64 | glog.Fatalf("error creating alert client: %v", err) 65 | } 66 | 67 | e := echo.New() 68 | e.Use(middleware.CORS()) 69 | e.Use(middleware.Logger()) 70 | 71 | handlers.RegisterBaseHandlers(e) 72 | handlers.RegisterV0Handlers(e, alertClient) 73 | handlers.RegisterV1Handlers(e, alertClient) 74 | 75 | glog.Infof("Prometheus Config server listening on port: %s\n", *port) 76 | e.Logger.Fatal(e.Start(fmt.Sprintf(":%s", *port))) 77 | } 78 | -------------------------------------------------------------------------------- /restrictor/query_restrictor.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package restrictor 9 | 10 | import ( 11 | "fmt" 12 | "strings" 13 | 14 | "github.com/prometheus/prometheus/pkg/labels" 15 | "github.com/prometheus/prometheus/promql/parser" 16 | ) 17 | 18 | // QueryRestrictor provides functionality to add restrictor labels to a 19 | // Prometheus query 20 | type QueryRestrictor struct { 21 | matchers []labels.Matcher 22 | Opts 23 | } 24 | 25 | // Opts contains optional configurations for the QueryRestrictor 26 | type Opts struct { 27 | ReplaceExistingLabel bool 28 | } 29 | 30 | var DefaultOpts = Opts{ReplaceExistingLabel: true} 31 | 32 | // NewQueryRestrictor returns a new QueryRestrictor to be built upon 33 | func NewQueryRestrictor(opts Opts) *QueryRestrictor { 34 | return &QueryRestrictor{ 35 | matchers: []labels.Matcher{}, 36 | Opts: opts, 37 | } 38 | } 39 | 40 | // AddMatcher takes a key and an arbitrary number of values. If only one value 41 | // is provided, an Equal matcher will be added to the restrictor. Otherwise a 42 | // Regex matcher with an OR of the values will be added. e.g. {label=~"value1|value2"} 43 | // If values is empty the label will be matched to the empty string e.g. {label=""} 44 | // effectively, this matches which do not contain this label 45 | func (q *QueryRestrictor) AddMatcher(key string, values ...string) *QueryRestrictor { 46 | if len(values) < 1 { 47 | q.matchers = append(q.matchers, labels.Matcher{Type: labels.MatchEqual, Name: key}) 48 | return q 49 | } 50 | 51 | if len(values) == 1 { 52 | q.matchers = append(q.matchers, labels.Matcher{Type: labels.MatchEqual, Name: key, Value: values[0]}) 53 | return q 54 | } 55 | 56 | q.matchers = append(q.matchers, labels.Matcher{Type: labels.MatchRegexp, Name: key, Value: strings.Join(values, "|")}) 57 | return q 58 | } 59 | 60 | // RestrictQuery appends a label selector to each metric in a given query so 61 | // that only metrics with those labels are returned from the query. 62 | func (q *QueryRestrictor) RestrictQuery(query string) (string, error) { 63 | if query == "" { 64 | return "", fmt.Errorf("empty query string") 65 | } 66 | 67 | promQuery, err := parser.ParseExpr(query) 68 | if err != nil { 69 | return "", fmt.Errorf("error parsing query: %v", err) 70 | } 71 | parser.Inspect(promQuery, q.addRestrictorLabels()) 72 | return promQuery.String(), nil 73 | } 74 | 75 | // Matchers returns the list of label matchers for the restrictor 76 | func (q *QueryRestrictor) Matchers() []labels.Matcher { 77 | return q.matchers 78 | } 79 | 80 | func (q *QueryRestrictor) addRestrictorLabels() func(n parser.Node, path []parser.Node) error { 81 | return func(n parser.Node, path []parser.Node) error { 82 | if n == nil { 83 | return nil 84 | } 85 | for _, matcher := range q.matchers { 86 | switch n := n.(type) { 87 | case *parser.VectorSelector: 88 | n.LabelMatchers = appendOrReplaceMatcher(n.LabelMatchers, matcher, q.ReplaceExistingLabel) 89 | case *parser.MatrixSelector: 90 | n.VectorSelector.(*parser.VectorSelector).LabelMatchers = appendOrReplaceMatcher(n.VectorSelector.(*parser.VectorSelector).LabelMatchers, matcher, q.ReplaceExistingLabel) 91 | } 92 | } 93 | return nil 94 | } 95 | } 96 | 97 | func appendOrReplaceMatcher(matchers []*labels.Matcher, newMatcher labels.Matcher, replaceExistingLabel bool) []*labels.Matcher { 98 | if replaceExistingLabel && getMatcherIndex(matchers, newMatcher.Name) >= 0 { 99 | return replaceLabelValue(matchers, newMatcher.Name, newMatcher.Value) 100 | } 101 | return append(matchers, &newMatcher) 102 | } 103 | 104 | func getMatcherIndex(matchers []*labels.Matcher, name string) int { 105 | for idx, match := range matchers { 106 | if match.Name == name { 107 | return idx 108 | } 109 | } 110 | return -1 111 | } 112 | 113 | func replaceLabelValue(matchers []*labels.Matcher, name, value string) []*labels.Matcher { 114 | idx := getMatcherIndex(matchers, name) 115 | if idx >= -1 { 116 | matchers[idx].Value = value 117 | } 118 | return matchers 119 | } 120 | -------------------------------------------------------------------------------- /restrictor/query_restrictor_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package restrictor 9 | 10 | import ( 11 | "testing" 12 | 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | type restrictorTestCase struct { 17 | name string 18 | input string 19 | expected string 20 | expectedError string 21 | restrictor *QueryRestrictor 22 | } 23 | 24 | func (tc *restrictorTestCase) RunTest(t *testing.T) { 25 | output, err := tc.restrictor.RestrictQuery(tc.input) 26 | if tc.expectedError == "" { 27 | assert.NoError(t, err) 28 | assert.Equal(t, tc.expected, output) 29 | return 30 | } 31 | assert.EqualError(t, err, tc.expectedError) 32 | } 33 | 34 | func TestQueryRestrictor_RestrictQuery(t *testing.T) { 35 | singleLabelRestrictor := NewQueryRestrictor(DefaultOpts).AddMatcher("networkID", "test") 36 | testCases := []*restrictorTestCase{ 37 | { 38 | name: "basic query", 39 | input: "up", 40 | expected: `up{networkID="test"}`, 41 | restrictor: singleLabelRestrictor, 42 | }, 43 | { 44 | name: "query with function", 45 | input: "sum(up)", 46 | expected: `sum(up{networkID="test"})`, 47 | restrictor: singleLabelRestrictor, 48 | }, 49 | { 50 | name: "query with labels", 51 | input: `up{label="value"}`, 52 | expected: `up{label="value",networkID="test"}`, 53 | restrictor: singleLabelRestrictor, 54 | }, 55 | { 56 | name: "query with multiple metrics", 57 | input: "metric1 or metric2", 58 | expected: `metric1{networkID="test"} or metric2{networkID="test"}`, 59 | restrictor: singleLabelRestrictor, 60 | }, 61 | { 62 | name: "query with multiple metrics and labels", 63 | input: `metric1 or metric2{label="value"}`, 64 | expected: `metric1{networkID="test"} or metric2{label="value",networkID="test"}`, 65 | restrictor: singleLabelRestrictor, 66 | }, 67 | { 68 | name: "query with matrix selector", 69 | input: "up[5m]", 70 | expected: `up{networkID="test"}[5m]`, 71 | restrictor: singleLabelRestrictor, 72 | }, 73 | { 74 | name: "query with matrix and functions", 75 | input: "sum_over_time(metric1[5m])", 76 | expected: `sum_over_time(metric1{networkID="test"}[5m])`, 77 | restrictor: singleLabelRestrictor, 78 | }, 79 | { 80 | name: "query with existing networkID", 81 | input: `metric1{networkID="test"}`, 82 | expected: `metric1{networkID="test"}`, 83 | restrictor: singleLabelRestrictor, 84 | }, 85 | { 86 | name: "query with existing wrong networkID", 87 | input: `metric1{networkID="malicious"}`, 88 | expected: `metric1{networkID="test"}`, 89 | restrictor: singleLabelRestrictor, 90 | }, 91 | { 92 | name: "restricts with multiple labels", 93 | input: `metric1`, 94 | expected: `metric1{newLabel1="value1",newLabel2="value2"}`, 95 | restrictor: NewQueryRestrictor(DefaultOpts).AddMatcher("newLabel1", "value1").AddMatcher("newLabel2", "value2"), 96 | }, 97 | { 98 | name: "creates an OR with multiple values", 99 | input: `metric1`, 100 | expected: `metric1{newLabel1=~"value1|value2"}`, 101 | restrictor: NewQueryRestrictor(DefaultOpts).AddMatcher("newLabel1", "value1", "value2"), 102 | }, 103 | { 104 | name: "creates an OR along with another label", 105 | input: `metric1{newLabel1="value1"}`, 106 | expected: `metric1{newLabel1="value1",newLabel2=~"value2|value3"}`, 107 | restrictor: NewQueryRestrictor(DefaultOpts).AddMatcher("newLabel2", "value2", "value3"), 108 | }, 109 | { 110 | name: "doesn't overwrite existing label if configured", 111 | input: `metric1{newLabel1="value1"}`, 112 | expected: `metric1{newLabel1="value1",newLabel1=~"value2|value3"}`, 113 | restrictor: NewQueryRestrictor(Opts{ReplaceExistingLabel: false}).AddMatcher("newLabel1", "value2", "value3"), 114 | }, 115 | { 116 | name: "Empty matcher value works", 117 | input: `metric1`, 118 | expected: `metric1{newLabel1=""}`, 119 | restrictor: NewQueryRestrictor(DefaultOpts).AddMatcher("newLabel1"), 120 | }, 121 | { 122 | name: "empty query", 123 | input: "", 124 | expectedError: "empty query string", 125 | restrictor: singleLabelRestrictor, 126 | }, 127 | } 128 | 129 | for _, test := range testCases { 130 | t.Run(test.name, test.RunTest) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /ui/.dockerignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | -------------------------------------------------------------------------------- /ui/.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/build/.* 3 | 4 | [include] 5 | 6 | [libs] 7 | 8 | [lints] 9 | 10 | [options] 11 | esproposal.optional_chaining=enable 12 | 13 | [strict] 14 | 15 | [version] 16 | ^0.124.0 17 | -------------------------------------------------------------------------------- /ui/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /ui/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # This source code is licensed under the MIT license found in the 3 | # LICENSE file in the root directory of this source tree. 4 | 5 | # pull official base image 6 | FROM node:13.12.0-alpine 7 | 8 | # set working directory 9 | WORKDIR /app 10 | 11 | # add `/app/node_modules/.bin` to $PATH 12 | ENV PATH /app/node_modules/.bin:$PATH 13 | 14 | # install app dependencies 15 | COPY package.json ./ 16 | RUN yarn install 17 | 18 | # add app 19 | COPY . ./ 20 | RUN yarn build 21 | 22 | # start app 23 | EXPOSE 3000 24 | CMD ["yarn", "start"] 25 | -------------------------------------------------------------------------------- /ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ui", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@fbcnms/alarms": "0.1.16", 7 | "@testing-library/jest-dom": "^5.11.9", 8 | "@testing-library/react": "^11.2.5", 9 | "@testing-library/user-event": "^12.7.1", 10 | "flow-bin": "^0.145.0", 11 | "notistack": "^1.0.3", 12 | "react": "^17.0.1", 13 | "react-dom": "^17.0.1", 14 | "react-router-dom": "^5.2.0", 15 | "react-scripts": "4.0.2" 16 | }, 17 | "scripts": { 18 | "start": "react-scripts start", 19 | "build": "react-scripts build", 20 | "test": "react-scripts test", 21 | "eject": "react-scripts eject", 22 | "flow": "flow" 23 | }, 24 | "eslintConfig": { 25 | "extends": "react-app" 26 | }, 27 | "browserslist": { 28 | "production": [ 29 | ">0.2%", 30 | "not dead", 31 | "not op_mini all" 32 | ], 33 | "development": [ 34 | "last 1 chrome version", 35 | "last 1 firefox version", 36 | "last 1 safari version" 37 | ] 38 | }, 39 | "devDependencies": { 40 | "@babel/cli": "^7.8.4", 41 | "@babel/core": "^7.9.6", 42 | "@babel/preset-flow": "^7.9.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /ui/public/index.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 16 | 17 | Alert Configuration 18 | 19 | 20 | 21 |
22 | 23 | 24 | -------------------------------------------------------------------------------- /ui/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Configmanager UI", 3 | "name": "Prometheus Configmanager UI", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "theme_color": "#000000", 7 | "background_color": "#ffffff" 8 | } 9 | -------------------------------------------------------------------------------- /ui/src/APIUtil.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2004-present Facebook. All Rights Reserved. 3 | * 4 | * @format 5 | * @flow strict-local 6 | */ 7 | 8 | import axios from 'axios'; 9 | import {useEffect, useState} from 'react'; 10 | import type {ApiUtil} from '@fbcnms/alarms/components/AlarmsApi'; 11 | import type {AxiosXHRConfig} from 'axios'; 12 | 13 | export const AM_BASE_URL = process.env.REACT_APP_AM_BASE_URL || ''; 14 | export const PROM_BASE_URL = process.env.REACT_APP_PROM_BASE_URL || ''; 15 | export const AM_CONFIG_URL = process.env.REACT_APP_AM_CONFIG_URL || ''; 16 | export const PROM_CONFIG_URL = process.env.REACT_APP_PROM_CONFIG_URL || ''; 17 | 18 | export function APIUtil(tenantID: string): ApiUtil { 19 | return { 20 | useAlarmsApi: useApi, 21 | 22 | // Alertmanager Requests 23 | viewFiringAlerts: async function(_req) { 24 | const resp = await makeRequest({ 25 | url: `${AM_BASE_URL}/alerts`, 26 | }); 27 | return resp.filter(alert => alert.labels.tenant === tenantID); 28 | }, 29 | viewMatchingAlerts: ({expression}) => 30 | makeRequest({url: `${AM_BASE_URL}/matching_alerts/${expression}`}), 31 | 32 | // suppressions 33 | getSuppressions: _req => 34 | makeRequest({ 35 | url: `${AM_BASE_URL}/silences`, 36 | method: 'GET', 37 | }), 38 | // global config 39 | getGlobalConfig: _req => 40 | makeRequest({url: `${AM_BASE_URL}/globalconfig`, method: 'GET'}), 41 | 42 | 43 | // Prometheus Configmanager Requests 44 | createAlertRule: ({rule}) => 45 | makeRequest({ 46 | url: `${PROM_CONFIG_URL}/${tenantID}/alert`, 47 | method: 'POST', 48 | data: rule, 49 | }), 50 | editAlertRule: ({rule}) => 51 | makeRequest({ 52 | url: `${PROM_CONFIG_URL}/${tenantID}/alert/${rule.alert}`, 53 | data: rule, 54 | method: 'PUT', 55 | }), 56 | getAlertRules: _req => 57 | makeRequest({ 58 | url: `${PROM_CONFIG_URL}/${tenantID}/alert`, 59 | method: 'GET', 60 | }), 61 | deleteAlertRule: ({ruleName}) => 62 | makeRequest({ 63 | url: `${PROM_CONFIG_URL}/${tenantID}/alert/${ruleName}`, 64 | method: 'DELETE', 65 | }), 66 | 67 | // Alertmanager Configurer Requests 68 | // receivers 69 | createReceiver: ({receiver}) => 70 | makeRequest({ 71 | url: `${AM_CONFIG_URL}/${tenantID}/receiver`, 72 | method: 'POST', 73 | data: receiver, 74 | }), 75 | editReceiver: ({receiver}) => 76 | makeRequest({ 77 | url: `${AM_CONFIG_URL}/${tenantID}/receiver/${receiver.name}`, 78 | method: 'PUT', 79 | data: receiver, 80 | }), 81 | getReceivers: _req => 82 | makeRequest({ 83 | url: `${AM_CONFIG_URL}/${tenantID}/receiver`, 84 | method: 'GET', 85 | }), 86 | deleteReceiver: ({receiverName}) => 87 | makeRequest({ 88 | url: `${AM_CONFIG_URL}/${tenantID}/receiver/${receiverName}`, 89 | method: 'DELETE', 90 | }), 91 | 92 | // routes 93 | getRouteTree: _req => 94 | makeRequest({ 95 | url: `${AM_CONFIG_URL}/${tenantID}/route`, 96 | method: 'GET', 97 | }), 98 | editRouteTree: req => 99 | makeRequest({ 100 | url: `${AM_CONFIG_URL}/${tenantID}/route`, 101 | method: 'POST', 102 | data: req.route, 103 | }), 104 | 105 | // metric series 106 | getMetricSeries: async function (_req) { 107 | const resp = await makeRequest({ 108 | url: `${PROM_BASE_URL}/series?match[]={__name__=~".*"}`, 109 | method: 'GET', 110 | }) 111 | return resp.data; 112 | }, 113 | 114 | editGlobalConfig: ({config}) => 115 | makeRequest({ 116 | url: `${AM_CONFIG_URL}/${tenantID}/globalconfig`, 117 | method: 'POST', 118 | data: config, 119 | }), 120 | getTenants: _req => 121 | makeRequest({ 122 | url: `${AM_CONFIG_URL}/tenants`, 123 | method: 'GET', 124 | }), 125 | getAlertmanagerTenancy: _req => 126 | makeRequest({ 127 | url: `${AM_CONFIG_URL}/tenancy`, 128 | method: 'GET', 129 | }), 130 | getPrometheusTenancy: _req => 131 | makeRequest({ 132 | url: `${PROM_CONFIG_URL}/tenancy`, 133 | method: 'GET', 134 | }) 135 | } 136 | }; 137 | 138 | function useApi( 139 | func: TParams => Promise, 140 | params: TParams, 141 | cacheCounter?: string | number, 142 | ): { 143 | response: ?TResponse, 144 | error: ?Error, 145 | isLoading: boolean, 146 | } { 147 | const [response, setResponse] = useState(); 148 | const [error, setError] = useState(null); 149 | const [isLoading, setIsLoading] = useState(true); 150 | const jsonParams = JSON.stringify(params); 151 | 152 | useEffect(() => { 153 | async function makeRequest() { 154 | try { 155 | const parsed = JSON.parse(jsonParams); 156 | setIsLoading(true); 157 | const res = await func(parsed); 158 | setResponse(res); 159 | setError(null); 160 | setIsLoading(false); 161 | } catch (err) { 162 | setError(err); 163 | setResponse(null); 164 | setIsLoading(false); 165 | } 166 | } 167 | makeRequest(); 168 | }, [jsonParams, func, cacheCounter]); 169 | 170 | return { 171 | error, 172 | response, 173 | isLoading, 174 | }; 175 | } 176 | 177 | async function makeRequest( 178 | axiosConfig: AxiosXHRConfig, 179 | ): Promise { 180 | const response = await axios(axiosConfig); 181 | return response.data; 182 | } 183 | -------------------------------------------------------------------------------- /ui/src/App.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | body { 9 | font-family: "Roboto", "Helvetica", "Arial", sans-serif; 10 | margin: auto; 11 | } 12 | -------------------------------------------------------------------------------- /ui/src/App.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // This source code is licensed under the MIT license found in the 3 | // LICENSE file in the root directory of this source tree. 4 | 5 | // @prettier 6 | // @flow 7 | 8 | import React from 'react'; 9 | import amber from '@material-ui/core/colors/amber'; 10 | import green from '@material-ui/core/colors/green'; 11 | import MuiStylesThemeProvider from '@material-ui/styles/ThemeProvider'; 12 | import TenantSelector from './TenantSelector'; 13 | 14 | import {BrowserRouter, Redirect, Route} from 'react-router-dom'; 15 | import {APIUtil} from './APIUtil'; 16 | import {Alarms} from '@fbcnms/alarms'; 17 | import {createMuiTheme} from '@material-ui/core/styles'; 18 | import {MuiThemeProvider} from '@material-ui/core/styles'; 19 | import {SnackbarProvider} from 'notistack'; 20 | import {useState} from 'react'; 21 | 22 | import type {Labels, TenancyConfig} from '@fbcnms/alarms/components/AlarmAPIType'; 23 | 24 | // default theme 25 | const theme = createMuiTheme({ 26 | palette: { 27 | success: { 28 | light: green[100], 29 | main: green[500], 30 | dark: green[800], 31 | }, 32 | warning: { 33 | light: amber[100], 34 | main: amber[500], 35 | dark: amber[800], 36 | }, 37 | // symphony theming 38 | secondary: { 39 | main: '#303846', 40 | }, 41 | grey: { 42 | '50': '#e4f0f6', 43 | }, 44 | }, 45 | }); 46 | 47 | 48 | function AlarmsMain() { 49 | const [tenantID, setTenantID] = useState("default"); 50 | 51 | const apiUtil = React.useMemo(() => APIUtil(tenantID),[tenantID]) 52 | 53 | // Initialize tenant if not already initialized 54 | const {error} = apiUtil.useAlarmsApi(apiUtil.getRouteTree, {tenantID}) 55 | React.useEffect(() => { 56 | if (error?.response?.status === 400 && 57 | error?.response?.data?.message?.includes('does not exist')) { 58 | APIUtil(tenantID).editRouteTree({ 59 | route: { 60 | receiver: `${tenantID}_tenant_base_route`, 61 | } 62 | }) 63 | } 64 | }, [error, tenantID]) 65 | 66 | 67 | const {response: amTenancyResp} = apiUtil.useAlarmsApi(apiUtil.getAlertmanagerTenancy, {}) 68 | const {response: promTenancyResp} = apiUtil.useAlarmsApi(apiUtil.getPrometheusTenancy, {}) 69 | 70 | const amTenancy: TenancyConfig = amTenancyResp ?? {restrictor_label: "", restrict_queries: false}; 71 | const promTenancy: TenancyConfig = promTenancyResp ?? {restrictor_label: "tenant", restrict_queries: false}; 72 | 73 | const isSingleTenant = amTenancy.restrictor_label === ""; 74 | 75 | const filterLabels = (labels: Labels): Labels => { 76 | const labelsToFilter = ['monitor', 'instance', 'job']; 77 | isSingleTenant && labelsToFilter.push(promTenancy.restrictor_label); 78 | const filtered = {...labels}; 79 | for (const label of labelsToFilter) { 80 | delete filtered[label]; 81 | } 82 | return filtered; 83 | } 84 | 85 | return( 86 | <> 87 | 88 | 89 | 96 | {isSingleTenant || 97 | 98 | } 99 | { 102 | return `${keyName}` 103 | } 104 | } 105 | alertManagerGlobalConfigEnabled={true} 106 | disabledTabs={['suppressions', 'routes']} 107 | thresholdEditorEnabled={true} 108 | filterLabels={filterLabels} 109 | /> 110 | 111 | 112 | 113 | 114 | ) 115 | } 116 | 117 | 118 | function App() { 119 | return ( 120 |
121 | 122 | }> 123 | 124 | 125 | 126 |
127 | ); 128 | } 129 | 130 | export default App; 131 | -------------------------------------------------------------------------------- /ui/src/TenantSelector.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // This source code is licensed under the MIT license found in the 3 | // LICENSE file in the root directory of this source tree. 4 | 5 | // @prettier 6 | // @flow 7 | 8 | import blue from '@material-ui/core/colors/blue'; 9 | import Button from '@material-ui/core/Button'; 10 | import Grid from '@material-ui/core/Grid'; 11 | import Menu from '@material-ui/core/Menu'; 12 | import MenuItem from '@material-ui/core/MenuItem'; 13 | import Modal from '@material-ui/core/Modal'; 14 | import React from 'react'; 15 | import TextField from '@material-ui/core/TextField'; 16 | 17 | import {APIUtil} from './APIUtil'; 18 | import {makeStyles} from '@material-ui/core/styles'; 19 | import {useState} from 'react'; 20 | 21 | import type {ApiUtil} from '@fbcnms/alarms/components/AlarmsApi'; 22 | 23 | const useStyles = makeStyles((theme) => ({ 24 | modal: { 25 | position: 'absolute', 26 | width: 400, 27 | backgroundColor: theme.palette.background.paper, 28 | padding: theme.spacing(2, 4, 3), 29 | top: `50%`, 30 | left: `50%`, 31 | transform: `translate(-50%, -50%)`, 32 | }, 33 | tenantMenu: { 34 | position: 'fixed', 35 | bottom: 5, 36 | left: 5, 37 | backgroundColor: blue[200], 38 | }, 39 | })); 40 | 41 | 42 | function CreateTenantModal(props: {open: boolean, setTenantId: (string) => void, onClose: () => void}) { 43 | const classes = useStyles(); 44 | const [newTenant, setNewTenant] = useState(""); 45 | 46 | const handleSave = () => { 47 | if (newTenant !== "") { 48 | props.setTenantId(newTenant); 49 | } 50 | APIUtil(newTenant).editRouteTree({ 51 | route: { 52 | receiver: `${newTenant}_tenant_base_route`, 53 | } 54 | }) 55 | props.onClose(); 56 | } 57 | 58 | const body = ( 59 |
60 |

New Tenant

61 | 62 | setNewTenant(e.target.value)} /> 63 | 64 | 65 |
66 | ); 67 | 68 | return ( 69 | 73 | {body} 74 | 75 | ); 76 | } 77 | 78 | export default function TenantSelector(props: {apiUtil: ApiUtil, setTenantId: (string) => void, tenantID: string}) { 79 | const classes = useStyles(); 80 | const [anchorEl, setAnchorEl] = React.useState(null); 81 | const [open, setOpen] = useState(false); 82 | 83 | const {response} = props.apiUtil.useAlarmsApi( 84 | props.apiUtil.getTenants, 85 | {open}, 86 | ); 87 | const tenantList = response ?? []; 88 | 89 | const handleClose = (event) => { 90 | if (event.target.dataset.tenantId) { 91 | props.setTenantId(event.target.dataset.tenantId) 92 | } 93 | setAnchorEl(null); 94 | } 95 | 96 | const handleModalClose = () => { 97 | setAnchorEl(null); 98 | setOpen(false); 99 | } 100 | 101 | return ( 102 | <> 103 | 106 | 113 | {tenantList.map(tenant => { 114 | return {tenant}; 115 | })} 116 | 117 | 120 | 121 | 122 | 123 | 124 | ) 125 | } 126 | -------------------------------------------------------------------------------- /ui/src/index.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. 2 | // This source code is licensed under the MIT license found in the 3 | // LICENSE file in the root directory of this source tree. 4 | 5 | import React from 'react'; 6 | import ReactDOM from 'react-dom'; 7 | import App from './App'; 8 | import './App.css'; 9 | 10 | ReactDOM.render( 11 | 12 | 13 | , 14 | document.getElementById('root') 15 | ); 16 | --------------------------------------------------------------------------------