mirror of
https://github.com/valitydev/mayday.git
synced 2024-11-06 02:35:22 +00:00
Implementation (#2)
* Project skeleton * WIP. Intermediate commit * WIP. Metric templates impl * WIP. Refactor + fixes * Dirty impl * Fix linter + imports * Trigger build * Add epic build * Fix ns property * Cleanup * Add missing fields * Fix alertmanagerconfig + logs * Remove empty group * Dirty fix * Another dirty fix * Fix receiver * Fix repeat interval param * Add new metrics * Fix templates * Fix templates * Fix prom queries * Impl dictionaries + new proto (#3) * Fix template reading * Fix query * Add logs * Fix query format * Support multiple values flow * Add more metrics + allow multiple values * Update metric configurations * Fix alert configurations * One more config fix * Update alert configurations * Update alert configurations * Fix alert configurations * Upd metric * Cleanup (#4) * Use feature based structure * Refactoring * WIP. Readme * Upd readme * Cleanup + filter notifications * Add more tests * Update README.md Co-authored-by: Alex Romanov <ex1tus@yandex.ru> * Update README.md Co-authored-by: Alex Romanov <ex1tus@yandex.ru> * Format shopIds (make it shorter) * Add wallets and limit metrics + minor fixes * Enable deploy from epic again * Fix npe + cleanup properties * Fix labels bug * Use more params for limits * Fix limits condition * Add more dictionaries * Fix method access modifier * Fix route naming * Fix rules naming * Add payout limits * Upd payout limit query * Alerts naming * One more limit fix * Remove duration option (#5) * Fix k8s rule config model * Aggregated metrics (#6) * Add aggregated metrics * Fix typo * Add currency to conversion (#7) * Add aggregated metrics * Fix typo * Add currency to conversion * Fix test --------- Co-authored-by: Alex Romanov <ex1tus@yandex.ru>
This commit is contained in:
parent
396cce59dd
commit
b1bc23ab8f
2
.github/settings.yml
vendored
Normal file
2
.github/settings.yml
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
# These settings are synced to GitHub by https://probot.github.io/apps/settings/
|
||||
_extends: .github
|
10
.github/workflows/basic-linters.yml
vendored
Normal file
10
.github/workflows/basic-linters.yml
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
name: Vality basic linters
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
uses: valitydev/base-workflows/.github/workflows/basic-linters.yml@v1
|
10
.github/workflows/build.yml
vendored
Normal file
10
.github/workflows/build.yml
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
name: Build Maven Artifact
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
uses: valitydev/base-workflow/.github/workflows/maven-service-build.yml@v2
|
14
.github/workflows/deploy.yml
vendored
Normal file
14
.github/workflows/deploy.yml
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
name: Deploy Docker Image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
- 'epic/**'
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
uses: valitydev/base-workflow/.github/workflows/maven-service-deploy.yml@v2
|
||||
secrets:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
mm-webhook-url: ${{ secrets.MATTERMOST_WEBHOOK_URL }}
|
79
.gitignore
vendored
Normal file
79
.gitignore
vendored
Normal file
@ -0,0 +1,79 @@
|
||||
# Created by .ignore support plugin (hsz.mobi)
|
||||
.eunit
|
||||
deps
|
||||
*.o
|
||||
*.beam
|
||||
*.plt
|
||||
erl_crash.dump
|
||||
ebin/*.beam
|
||||
rel/example_project
|
||||
.concrete/DEV_MODE
|
||||
.rebar
|
||||
target/
|
||||
pom.xml.tag
|
||||
pom.xml.releaseBackup
|
||||
pom.xml.versionsBackup
|
||||
pom.xml.next
|
||||
release.properties
|
||||
dependency-reduced-pom.xml
|
||||
buildNumber.properties
|
||||
.mvn/timing.properties
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff:
|
||||
.idea/
|
||||
.idea/workspace.xml
|
||||
.idea/tasks.xml
|
||||
.idea/dictionaries
|
||||
.idea/vcs.xml
|
||||
.idea/jsLibraryMappings.xml
|
||||
.DS_Store
|
||||
|
||||
# Sensitive or high-churn files:
|
||||
.idea/dataSources.ids
|
||||
.idea/dataSources.xml
|
||||
.idea/dataSources.local.xml
|
||||
.idea/sqlDataSources.xml
|
||||
.idea/dynamic.xml
|
||||
.idea/uiDesigner.xml
|
||||
|
||||
# Gradle:
|
||||
.idea/gradle.xml
|
||||
.idea/libraries
|
||||
|
||||
# Mongo Explorer plugin:
|
||||
.idea/mongoSettings.xml
|
||||
|
||||
*.iws
|
||||
*.ipr
|
||||
*.iml
|
||||
|
||||
|
||||
# IntelliJ
|
||||
/out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
*.class
|
||||
|
||||
# Mobile Tools for Java (J2ME)
|
||||
.mtj.tmp/
|
||||
|
||||
# Package Files #
|
||||
*.jar
|
||||
*.war
|
||||
*.ear
|
||||
|
||||
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
||||
hs_err_pid*
|
||||
env.list
|
1
CODEOWNERS
Normal file
1
CODEOWNERS
Normal file
@ -0,0 +1 @@
|
||||
* @valitydev/java
|
176
LICENSE
Normal file
176
LICENSE
Normal file
@ -0,0 +1,176 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
184
README.md
184
README.md
@ -1 +1,183 @@
|
||||
# mayday
|
||||
# mayday
|
||||
|
||||
Сервис алертинга, отвечающий за управление конфигурациями алертов
|
||||
|
||||
## Роль сервиса в системе алертинга
|
||||
|
||||
Приблизительная схема системы алертинга выглядит следующим образом:
|
||||
|
||||
![схема системы алертинга](./img/alerting_common.svg)
|
||||
|
||||
Часть системы алертинга, в которой участвует mayday:
|
||||
|
||||
![схема системы алертинга_мэйдэй](./img/mayday_common.svg)
|
||||
|
||||
В рамках данного документа будут объяснены все взаимодействия со схемы выше.
|
||||
|
||||
## Внутреннее устройство сервиса
|
||||
|
||||
### Структура проекта
|
||||
|
||||
Проект использует [feature-layered](https://phauer.com/2020/package-by-feature/) структуру пакетов.
|
||||
Взаимодействие с prometheus, взаимодействие с alertmanager, формирование конфигураций алертов и протокол для взаимодействия с сервисом разнесены по разным корневым каталогам.
|
||||
Это позволяет вносить правки в конкретную "фичу" и не бояться, что они сломают соседний функционал.
|
||||
В сервисе есть функционал, которые переиспользуются для всех "фич" - он вынесен в пакет [common](src/main/java/dev/vality/alerting/mayday/common).
|
||||
|
||||
Если вас интересует не определенный функционал сервиса, а общее понимание его устройства, то лучше начать изучение кодовой базы с класса [AlertingService](src/main/java/dev/vality/alerting/mayday/thrift/service/AlertingService.java) - он реализует [thrift-протокол](https://github.com/valitydev/mayday-proto), через который и осуществляется взаимодействие с сервисом.
|
||||
|
||||
##### Prometheus
|
||||
|
||||
Prometheus содержит информацию о всех метриках, которые отдают ему экспортеры. С помощью правил алертинга (они же [alerting rules](https://prometheus.io/docs/prometheus/latest/configuration/alerting_rules/)) можно настроить prometheus таким образом, чтобы он на постоянной основе выполнял переданный в правиле алертинга запрос к метрикам. Если результат запроса "положителен" (`true`/ не `0`) дольше, чем период времени, который так же передается в правиле алертинга, prometheus отправляет запрос в alertmanager.
|
||||
|
||||
Сервис mayday отвечает за создание и удаление правил алертинга.
|
||||
|
||||
API, через которое происходит взаимодействие с prometheus, описано в разделе [prometheus-operator](#prometheus-operator).
|
||||
|
||||
##### Alertmanager
|
||||
|
||||
Alertmanager содержит информацию о том, кому, куда и с какой периодичностью отправлять алерты. Чтобы лучше понимать специфику алертменеджера, рекомендуется изучить официальную [документацию](https://prometheus.io/docs/alerting/latest/alertmanager/).
|
||||
|
||||
В рамках системы алертинга, сервис mayday создает новый маршрут([route](https://prometheus.io/docs/alerting/latest/configuration/#route)) в конфигурации алертменеджера для каждого нового правила алертинга.
|
||||
Когда алертменеджер отправляет алерт, в качестве способа отправки используется [webhook](https://prometheus.io/docs/alerting/latest/configuration/#webhook_config), а в качестве адресата - сервис mayday (вебхук прилетает в [контроллер](src/main/java/dev/vality/alerting/mayday/alertmanager/controller/WebhookController.java)). Mayday на основе содержимого вебхука определяет получателся алерта и "прокидывает" его дальше, предварительно преобразовав в нужный формат - в данный момент единственным получателем является сервис [alert-tg-bot](https://github.com/valitydev/alert-tg-bot).
|
||||
|
||||
API, через которое происходит взаимодействие сервиса mayday с alertmanager, описано в разделе [prometheus-operator](#prometheus-operator).
|
||||
|
||||
#### Prometheus-operator
|
||||
|
||||
Сервис "общается" с prometheus'ом и alertmanager'ом через [API](https://prometheus-operator.dev/docs/operator/api/), которое предоставляет [prometheus-operator](https://prometheus-operator.dev/).
|
||||
|
||||
В случае с prometheus, mayday создает одну группу правил ([RuleGroup](https://prometheus-operator.dev/docs/operator/api/#monitoring.coreos.com/v1.RuleGroup)) с именем `mayday-managed-rule`. Далее, при получении запроса на создание алерта, внутри этой группы создается отдельное [правило](https://prometheus-operator.dev/docs/operator/api/#monitoring.coreos.com/v1.Rule) на каждый алерт. Реализацию этой логики можно изучить в коде [клиента prometheus](src/main/java/dev/vality/alerting/mayday/prometheus/client/k8s/PrometheusClient.java).
|
||||
|
||||
В случае с alertmanager, mayday создает одну конфигурацию алертменеджера ([AlertmanagerConfig](https://prometheus-operator.dev/docs/operator/api/#monitoring.coreos.com/v1alpha1.AlertmanagerConfig)) с именем `mayday-managed-config`. Далее, при получении запроса на создание алерта, внутри этой конфигурации создается отдельный [маршрут](https://prometheus-operator.dev/docs/operator/api/#monitoring.coreos.com/v1alpha1.Route) на каждый алерт. Реализацию этой логики можно изучить в коде [клиента alertmanager](src/main/java/dev/vality/alerting/mayday/alertmanager/client/k8s/AlertmanagerClient.java).
|
||||
|
||||
#### Конфигурации алертов
|
||||
|
||||
Алерты, которые поддерживает система алертинга, описаны в виде json-конфигураций и хранятся [тут](src/main/resources/template).
|
||||
Рассмотрим одну из таких конфигураций (в урезанном виде):
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "payment_conversion",
|
||||
"readable_name": "Конверсия платежей",
|
||||
"prometheus_query": "round(100 * sum(sum_over_time(ebm_payments_final_status_count{provider_id=~\"${provider_id}\", terminal_id=~\"${terminal_id}\",shop_id=~\"${shop_id}\",status=\"captured\"}[${conversion_period_minutes}m])) / sum(sum_over_time(ebm_payments_final_status_count{provider_id=~\"${provider_id}\",terminal_id=~\"${terminal_id}\",shop_id=~\"${shop_id}\",status=~\"captured|failed\"}[${conversion_period_minutes}m])), 1) ${boundary_type} ${conversion_rate_threshold}",
|
||||
"alert_name_template": "Конверсия платежей по провайдеру '${provider_id}', терминалу '${terminal_id}' и магазину '${shop_id}' за последние ${conversion_period_minutes} минут ${boundary_type} ${conversion_rate_threshold}%",
|
||||
"alert_notification_template": "Конверсия платежей по провайдеру '${provider_id}', терминалу '${terminal_id}' и магазину '${shop_id}' за последние ${conversion_period_minutes} минут ${boundary_type} ${conversion_rate_threshold}%! Текущее значение: {{ $value }}%",
|
||||
"parameters": [
|
||||
{
|
||||
"id": 1,
|
||||
"substitution_name": "provider_id",
|
||||
"readable_name": "Введите идентификатор провайдера (опционально)",
|
||||
"mandatory": false,
|
||||
"multiple_values": true,
|
||||
"dictionary_name": "providers"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Поле | Описание |
|
||||
|-------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `id` | Уникальный идентификатор алерта |
|
||||
| `readable_name` | Название алерта. Выводится пользователю, когда он хочет увидеть доступные к созданию алерты |
|
||||
| `prometheus_query` | Запрос в прометеус, который будет выполнятся в рамках [alerting rules](https://prometheus.io/docs/prometheus/latest/configuration/alerting_rules/). Запрос в конфигурации содержит переменные (в формате `${variable_name}`), которые заменяются реальными значениями |
|
||||
| `alert_name_template` | Шаблон с названием уже созданного алерта. Выводится пользователю, когда он хочет увидеть свои созданные алерты |
|
||||
| `alert_notification_template` | Шаблон алерта. Это сообщение присылается пользователю, когда срабатывает алерт |
|
||||
| `parameters` | Массив параметров, которые должен задать пользователь, чтобы mayday смог создать алерт |
|
||||
|
||||
Каждый параметр состоит из набора полей:
|
||||
|
||||
|
||||
| Поле | Описание |
|
||||
|---------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `id` | Уникальный числовой идентификатор параметра. Служит для сортировки параметров (чтобы они гарантированно выводились пользователю в определенном порядке). |
|
||||
| `substitution_name` | Название переменной, в которое будет подставляться значение, которое передал пользователь. Значение подставляется в поля `prometheus_query`, `alert_name_template`, `alert_notification_template` описанные выше |
|
||||
| `readable_name` | Название параметра. Выводится пользователю, чтобы он понял, какая информация от него требуется |
|
||||
| `mandatory` | Флаг обязательности параметра. Если `false`, то пользователь может не вводить этот параметр |
|
||||
| `multiple_values` | Флаг, определяющий, поддерживает ли параметр несколько значений |
|
||||
| `dictionary_name` | В случае, когда пользователь не должен вводить значение параметра, а должен выбрать его из списка значений, в данном поле указывается название справочника. Поддерживаемые справочники описаны [здесь](src/main/java/dev/vality/alerting/mayday/alerttemplate/model/alerttemplate/DictionaryType.java) |
|
||||
| `regexp` | В случае, когда пользователь должен вводить значение параметра, здесь указывается регулярное выражение, с помощью которого можно провести валидацию ввода |
|
||||
|
||||
|
||||
#### Thrift
|
||||
|
||||
Взаимодействие с сервисом осуществляется через [протокол](https://github.com/valitydev/mayday-proto). Протокол реализует класс [AlertingService](src/main/java/dev/vality/alerting/mayday/thrift/service/AlertingService.java).
|
||||
|
||||
### Запуск и отладка сервиса на локальной машине
|
||||
|
||||
Для запуска сервиса на локальной машине требуется следующее:
|
||||
|
||||
1. Запустить k8s. Например, в рамках Docker Desktop: Settings -> Kubernetes -> Enable Kubernetes
|
||||
![docker_k8s.png](img/docker_k8s.png)
|
||||
2. Выполнить все шаги разделов [Getting started](https://prometheus-operator.dev/docs/user-guides/getting-started/) и [Alerting](https://prometheus-operator.dev/docs/user-guides/alerting/). В результате на вашей машине будет локально работающий кластер k8s с установленным оператором прометеус и работающей связкой prometheus + alertmanager.
|
||||
3. Запустить сервис mayday (прямо из IDE)
|
||||
|
||||
Готово. Теперь можно отлаживать работу сервиса с помощью [woorl'а](https://github.com/valitydev/woorl).
|
||||
|
||||
Примеры запросов:
|
||||
|
||||
Получить список поддерживаемых алертов:
|
||||
```
|
||||
woorl -s /mayday-proto/proto/mayday.thrift 'http://localhost:8022/mayday' AlertingService GetSupportedAlerts
|
||||
```
|
||||
|
||||
Получить алерты пользователя:
|
||||
```
|
||||
woorl -s /mayday-proto/proto/mayday.thrift 'http://localhost:8022/mayday' AlertingService GetUserAlerts '"username"'
|
||||
```
|
||||
|
||||
Удалить алерт пользователя:
|
||||
```
|
||||
woorl -s /mayday-proto/proto/mayday.thrift 'http://localhost:8022/mayday' AlertingService DeleteAlert '"username"' '"323de93494df90733484f94f1afd2b44"'
|
||||
```
|
||||
|
||||
Создать алерт:
|
||||
|
||||
```
|
||||
woorl -s /mayday-proto/proto/mayday.thrift 'http://localhost:8022/mayday' AlertingService CreateAlert
|
||||
'{
|
||||
"alert_id": "payment_conversion",
|
||||
"parameters": [
|
||||
{
|
||||
"id": "1",
|
||||
"value": "(1) test_provider"
|
||||
},
|
||||
{
|
||||
"id": "1",
|
||||
"value": "(2) test_tete"
|
||||
},
|
||||
{
|
||||
"id": "1",
|
||||
"value": "-"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"value": "-"
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"value": "(test_shop) shop details"
|
||||
},
|
||||
{
|
||||
"id": "4",
|
||||
"value": "Меньше порогового значения"
|
||||
},
|
||||
{
|
||||
"id": "5",
|
||||
"value": "50"
|
||||
},
|
||||
{
|
||||
"id": "6",
|
||||
"value": "60"
|
||||
},
|
||||
{
|
||||
"id": "rule_check_duration_minutes",
|
||||
"value": "5"
|
||||
},
|
||||
{
|
||||
"id": "alert_repeat_minutes",
|
||||
"value": "60"
|
||||
}
|
||||
],
|
||||
"user_id": "username"
|
||||
}'
|
||||
```
|
||||
|
4
img/alerting_common.svg
Normal file
4
img/alerting_common.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 321 KiB |
BIN
img/docker_k8s.png
Normal file
BIN
img/docker_k8s.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 63 KiB |
4
img/mayday_common.svg
Normal file
4
img/mayday_common.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 314 KiB |
192
pom.xml
Normal file
192
pom.xml
Normal file
@ -0,0 +1,192 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>dev.vality</groupId>
|
||||
<artifactId>service-parent-pom</artifactId>
|
||||
<version>2.1.5</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>mayday</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>mayday</name>
|
||||
<description>mayday</description>
|
||||
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
<java.version>17</java.version>
|
||||
<server.port>8022</server.port>
|
||||
<management.port>8023</management.port>
|
||||
<exposed.ports>${management.port} ${server.port}</exposed.ports>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-dependencies</artifactId>
|
||||
<version>2022.0.3</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
<!-- vality -->
|
||||
<dependency>
|
||||
<groupId>dev.vality</groupId>
|
||||
<artifactId>mayday-proto</artifactId>
|
||||
<version>1.8-d79c25b</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>dev.vality</groupId>
|
||||
<artifactId>alert-tg-bot-proto</artifactId>
|
||||
<version>1.4-adf83ed</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>dev.vality</groupId>
|
||||
<artifactId>db-common-lib</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>dev.vality</groupId>
|
||||
<artifactId>msgpack-proto</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>dev.vality.geck</groupId>
|
||||
<artifactId>common</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- spring -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-jdbc</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!--third party-->
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.micrometer</groupId>
|
||||
<artifactId>micrometer-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.micrometer</groupId>
|
||||
<artifactId>micrometer-registry-prometheus</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
<artifactId>commons-io</artifactId>
|
||||
<version>2.11.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>javax.servlet</groupId>
|
||||
<artifactId>javax.servlet-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.fabric8</groupId>
|
||||
<artifactId>kubernetes-client</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.hibernate.validator</groupId>
|
||||
<artifactId>hibernate-validator</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- test -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>dev.vality</groupId>
|
||||
<artifactId>testcontainers-annotations</artifactId>
|
||||
<version>2.0.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<sourceDirectory>${project.basedir}/src/main/java</sourceDirectory>
|
||||
<testSourceDirectory>${project.basedir}/src/test/java</testSourceDirectory>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>${project.build.directory}/maven-shared-archive-resources</directory>
|
||||
<targetPath>${project.build.directory}</targetPath>
|
||||
<includes>
|
||||
<include>Dockerfile</include>
|
||||
</includes>
|
||||
<filtering>true</filtering>
|
||||
</resource>
|
||||
<resource>
|
||||
<directory>${project.build.directory}/maven-shared-archive-resources</directory>
|
||||
<filtering>true</filtering>
|
||||
<excludes>
|
||||
<exclude>Dockerfile</exclude>
|
||||
</excludes>
|
||||
</resource>
|
||||
<resource>
|
||||
<directory>src/main/resources</directory>
|
||||
<filtering>true</filtering>
|
||||
</resource>
|
||||
</resources>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-remote-resources-plugin</artifactId>
|
||||
<version>3.0.0</version>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.apache.maven.shared</groupId>
|
||||
<artifactId>maven-filtering</artifactId>
|
||||
<version>3.3.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<configuration>
|
||||
<resourceBundles>
|
||||
<resourceBundle>dev.vality:shared-resources:${shared-resources.version}</resourceBundle>
|
||||
</resourceBundles>
|
||||
<attachToMain>false</attachToMain>
|
||||
<attachToTest>false</attachToTest>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>process</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
4
renovate.json
Normal file
4
renovate.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["local>valitydev/.github:renovate-config"]
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package dev.vality.alerting.mayday;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.web.servlet.ServletComponentScan;
|
||||
|
||||
@ServletComponentScan
|
||||
@SpringBootApplication
|
||||
public class MaydayApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(MaydayApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
package dev.vality.alerting.mayday.alertmanager.client.k8s;
|
||||
|
||||
import dev.vality.alerting.mayday.alertmanager.client.k8s.model.AlertmanagerConfig;
|
||||
import dev.vality.alerting.mayday.alertmanager.client.k8s.model.AlertmanagerConfigSpec;
|
||||
import dev.vality.alerting.mayday.alertmanager.client.k8s.util.AlertmanagerFunctionsUtil;
|
||||
import io.fabric8.kubernetes.api.model.KubernetesResourceList;
|
||||
import io.fabric8.kubernetes.client.Config;
|
||||
import io.fabric8.kubernetes.client.KubernetesClient;
|
||||
import io.fabric8.kubernetes.client.KubernetesClientBuilder;
|
||||
import io.fabric8.kubernetes.client.KubernetesClientException;
|
||||
import io.fabric8.kubernetes.client.dsl.MixedOperation;
|
||||
import io.fabric8.kubernetes.client.dsl.Resource;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class AlertmanagerClient {
|
||||
|
||||
private final Config k8sConfig;
|
||||
|
||||
public Optional<AlertmanagerConfig> getAlertmanagerConfig(String alertmanagerConfigName)
|
||||
throws KubernetesClientException {
|
||||
try (KubernetesClient client = new KubernetesClientBuilder().withConfig(k8sConfig).build()) {
|
||||
MixedOperation<AlertmanagerConfig, KubernetesResourceList<AlertmanagerConfig>, Resource<AlertmanagerConfig>>
|
||||
alertmanagerConfigClient = client.resources(AlertmanagerConfig.class);
|
||||
var config = alertmanagerConfigClient.withName(alertmanagerConfigName).get();
|
||||
return Optional.ofNullable(config);
|
||||
}
|
||||
}
|
||||
|
||||
public void createAlertmanagerConfig(AlertmanagerConfig alertmanagerConfig) {
|
||||
try (KubernetesClient client = new KubernetesClientBuilder().withConfig(k8sConfig).build()) {
|
||||
MixedOperation<AlertmanagerConfig, KubernetesResourceList<AlertmanagerConfig>, Resource<AlertmanagerConfig>>
|
||||
alertmanagerConfigClient = client.resources(AlertmanagerConfig.class);
|
||||
try {
|
||||
alertmanagerConfigClient.inNamespace(client.getNamespace()).resource(alertmanagerConfig).create();
|
||||
} catch (KubernetesClientException e) {
|
||||
// 409 http код возникает при попытке создать уже существующий объект
|
||||
if (!e.getStatus().getCode().equals(HttpStatus.CONFLICT.value())) {
|
||||
throw e;
|
||||
}
|
||||
log.warn("Tried to create already existing alertmanager config", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void addRouteIfNotExists(String configName,
|
||||
AlertmanagerConfigSpec.ChildRoute route) {
|
||||
modifyAlertmanagerConfig(configName, AlertmanagerFunctionsUtil.getAddRouteFunc(route));
|
||||
}
|
||||
|
||||
private void modifyAlertmanagerConfig(String configName,
|
||||
UnaryOperator<AlertmanagerConfig> modifyFunc)
|
||||
throws KubernetesClientException {
|
||||
try (KubernetesClient client = new KubernetesClientBuilder().withConfig(k8sConfig).build()) {
|
||||
MixedOperation<AlertmanagerConfig, KubernetesResourceList<AlertmanagerConfig>, Resource<AlertmanagerConfig>>
|
||||
alertmanagerConfigClient = client.resources(AlertmanagerConfig.class);
|
||||
var config =
|
||||
alertmanagerConfigClient.inNamespace(client.getNamespace()).withName(configName).get();
|
||||
var resource = alertmanagerConfigClient.inNamespace(client.getNamespace()).resource(config);
|
||||
resource.edit(modifyFunc);
|
||||
}
|
||||
}
|
||||
|
||||
public void deleteRoute(String configName, String alertId) {
|
||||
modifyAlertmanagerConfig(configName, AlertmanagerFunctionsUtil.getRemoveRouteByAlertIdFunc(alertId));
|
||||
}
|
||||
|
||||
public void deleteRoutes(String configName, String userId) {
|
||||
modifyAlertmanagerConfig(configName, AlertmanagerFunctionsUtil.getRemoveUserRoutesFunc(userId));
|
||||
}
|
||||
|
||||
public boolean containsRoute(String configName,
|
||||
String userId,
|
||||
String alertname) {
|
||||
Optional<AlertmanagerConfig> configOptional = getAlertmanagerConfig(configName);
|
||||
if (configOptional.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
AlertmanagerConfig alertmanagerConfig = configOptional.get();
|
||||
return AlertmanagerFunctionsUtil.hasRoute(alertmanagerConfig, userId, alertname);
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package dev.vality.alerting.mayday.alertmanager.client.k8s.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import io.fabric8.kubernetes.api.model.Namespaced;
|
||||
import io.fabric8.kubernetes.client.CustomResource;
|
||||
import io.fabric8.kubernetes.model.annotation.Group;
|
||||
import io.fabric8.kubernetes.model.annotation.Kind;
|
||||
import io.fabric8.kubernetes.model.annotation.Version;
|
||||
|
||||
@Version("v1alpha1")
|
||||
@Group("monitoring.coreos.com")
|
||||
@Kind("AlertmanagerConfig")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class AlertmanagerConfig extends CustomResource<AlertmanagerConfigSpec, AlertmanagerConfigStatus>
|
||||
implements Namespaced {
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
package dev.vality.alerting.mayday.alertmanager.client.k8s.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
@Data
|
||||
@RequiredArgsConstructor
|
||||
public class AlertmanagerConfigSpec {
|
||||
|
||||
private Route route;
|
||||
private Set<Receiver> receivers;
|
||||
|
||||
|
||||
@Data
|
||||
public static class Route {
|
||||
private String receiver;
|
||||
private Set<Matcher> matchers;
|
||||
private Set<String> groupBy;
|
||||
private String groupWait;
|
||||
private String groupInterval;
|
||||
private String repeatInterval;
|
||||
@JsonProperty("continue")
|
||||
private Boolean continueRouteLookUp;
|
||||
private Set<ChildRoute> routes = new HashSet<>();
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class ChildRoute {
|
||||
private String receiver;
|
||||
private Set<String> groupBy;
|
||||
private Set<Matcher> matchers;
|
||||
private String groupWait;
|
||||
private String groupInterval;
|
||||
private String repeatInterval;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class Receiver {
|
||||
private String name;
|
||||
private Set<WebhookConfig> webhookConfigs;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class WebhookConfig {
|
||||
private String url;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class Matcher {
|
||||
private String name;
|
||||
private String value;
|
||||
private String matchType;
|
||||
private boolean regex;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
package dev.vality.alerting.mayday.alertmanager.client.k8s.model;
|
||||
|
||||
public class AlertmanagerConfigStatus {
|
||||
}
|
@ -0,0 +1,114 @@
|
||||
package dev.vality.alerting.mayday.alertmanager.client.k8s.util;
|
||||
|
||||
import dev.vality.alerting.mayday.alertmanager.client.k8s.model.AlertmanagerConfig;
|
||||
import dev.vality.alerting.mayday.alertmanager.client.k8s.model.AlertmanagerConfigSpec;
|
||||
import dev.vality.alerting.mayday.common.constant.PrometheusRuleAnnotation;
|
||||
import dev.vality.alerting.mayday.common.constant.PrometheusRuleLabel;
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
@UtilityClass
|
||||
public class AlertmanagerFunctionsUtil {
|
||||
|
||||
public static UnaryOperator<AlertmanagerConfig> getRemoveRouteByAlertIdFunc(String alertId) {
|
||||
return alertmanagerConfig -> {
|
||||
var routes = alertmanagerConfig.getSpec().getRoute().getRoutes();
|
||||
if (routes == null || routes.isEmpty()) {
|
||||
return alertmanagerConfig;
|
||||
}
|
||||
var routesIterator = routes.iterator();
|
||||
while (routesIterator.hasNext()) {
|
||||
var route = routesIterator.next();
|
||||
var matchers = route.getMatchers();
|
||||
var alertNameMatcher = createMatcher(PrometheusRuleAnnotation.ALERT_NAME, alertId);
|
||||
if (matchers != null && matchers.contains(alertNameMatcher)) {
|
||||
routesIterator.remove();
|
||||
break;
|
||||
}
|
||||
}
|
||||
return alertmanagerConfig;
|
||||
};
|
||||
}
|
||||
|
||||
public static AlertmanagerConfigSpec.Matcher createMatcher(String labelName, String labelValue) {
|
||||
AlertmanagerConfigSpec.Matcher matcher = new AlertmanagerConfigSpec.Matcher();
|
||||
matcher.setName(labelName);
|
||||
matcher.setValue(labelValue);
|
||||
matcher.setMatchType("=");
|
||||
matcher.setRegex(false);
|
||||
return matcher;
|
||||
}
|
||||
|
||||
public static UnaryOperator<AlertmanagerConfig> getRemoveUserRoutesFunc(String userId) {
|
||||
return alertmanagerConfig -> {
|
||||
if (alertmanagerConfig.getSpec() == null) {
|
||||
return alertmanagerConfig;
|
||||
}
|
||||
|
||||
var routes = alertmanagerConfig.getSpec().getRoute().getRoutes();
|
||||
if (routes == null || routes.isEmpty()) {
|
||||
return alertmanagerConfig;
|
||||
}
|
||||
var routesIterator = routes.iterator();
|
||||
while (routesIterator.hasNext()) {
|
||||
var route = routesIterator.next();
|
||||
var matchers = route.getMatchers();
|
||||
var userMatcher = createMatcher(PrometheusRuleLabel.USERNAME, userId);
|
||||
if (matchers.contains(userMatcher)) {
|
||||
routesIterator.remove();
|
||||
}
|
||||
}
|
||||
return alertmanagerConfig;
|
||||
};
|
||||
}
|
||||
|
||||
public static UnaryOperator<AlertmanagerConfig> getAddRouteFunc(
|
||||
AlertmanagerConfigSpec.ChildRoute route) {
|
||||
return alertmanagerConfig -> {
|
||||
|
||||
if (!hasRoute(alertmanagerConfig, route)) {
|
||||
var configRoute = alertmanagerConfig.getSpec().getRoute() == null
|
||||
? new AlertmanagerConfigSpec.Route() : alertmanagerConfig.getSpec().getRoute();
|
||||
configRoute.getRoutes().add(route);
|
||||
alertmanagerConfig.getSpec().setRoute(configRoute);
|
||||
}
|
||||
|
||||
return alertmanagerConfig;
|
||||
};
|
||||
}
|
||||
|
||||
private static boolean hasRoute(AlertmanagerConfig alertmanagerConfig, AlertmanagerConfigSpec.ChildRoute route) {
|
||||
if (alertmanagerConfig.getSpec().getRoute() == null
|
||||
|| alertmanagerConfig.getSpec().getRoute().getRoutes() == null) {
|
||||
return false;
|
||||
}
|
||||
return alertmanagerConfig.getSpec().getRoute().getRoutes().stream()
|
||||
.anyMatch(childRoute -> childRoute.equals(route));
|
||||
}
|
||||
|
||||
public static boolean hasRoute(AlertmanagerConfig alertmanagerConfig, String userId, String alertName) {
|
||||
if (alertmanagerConfig.getSpec().getRoute() == null
|
||||
|| alertmanagerConfig.getSpec().getRoute().getRoutes() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
AlertmanagerConfigSpec.Matcher userIdMatcher =
|
||||
AlertmanagerFunctionsUtil.createMatcher(PrometheusRuleLabel.USERNAME,
|
||||
userId);
|
||||
AlertmanagerConfigSpec.Matcher alertNameMatcher =
|
||||
AlertmanagerFunctionsUtil.createMatcher(PrometheusRuleLabel.ALERT_NAME,
|
||||
alertName);
|
||||
Set<AlertmanagerConfigSpec.ChildRoute> routes = alertmanagerConfig.getSpec().getRoute().getRoutes();
|
||||
for (AlertmanagerConfigSpec.ChildRoute route : routes) {
|
||||
var matchers = route.getMatchers();
|
||||
if (matchers.contains(alertNameMatcher) && matchers.contains(userIdMatcher)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package dev.vality.alerting.mayday.alertmanager.config;
|
||||
|
||||
import dev.vality.alerting.mayday.alertmanager.config.properties.TelegramBotReceiverProperties;
|
||||
import dev.vality.alerting.tg_bot.NotifierServiceSrv;
|
||||
import dev.vality.woody.thrift.impl.http.THSpawnClientBuilder;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@Configuration
|
||||
public class TgBotReceiverConfig {
|
||||
|
||||
@Bean
|
||||
public NotifierServiceSrv.Iface telegramBotClient(TelegramBotReceiverProperties properties) throws IOException {
|
||||
return new THSpawnClientBuilder()
|
||||
.withAddress(properties.getUrl().getURI())
|
||||
.withNetworkTimeout(properties.getNetworkTimeout())
|
||||
.build(NotifierServiceSrv.Iface.class);
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package dev.vality.alerting.mayday.alertmanager.config.properties;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "alertmanager.webhook")
|
||||
@Validated
|
||||
@Getter
|
||||
@Setter
|
||||
public class AlertmanagerWebhookProperties {
|
||||
@NotNull
|
||||
private String url;
|
||||
private String path;
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package dev.vality.alerting.mayday.alertmanager.config.properties;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "k8s.alertmanager-configuration")
|
||||
@Getter
|
||||
@Setter
|
||||
public class K8sAlertmanagerProperties {
|
||||
private Map<String, String> labels;
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package dev.vality.alerting.mayday.alertmanager.config.properties;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "alertmanager.receiver.telegram-bot")
|
||||
@Validated
|
||||
@Getter
|
||||
@Setter
|
||||
public class TelegramBotReceiverProperties {
|
||||
@NotNull
|
||||
private Resource url;
|
||||
private int networkTimeout = 5000;
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
package dev.vality.alerting.mayday.alertmanager.constant;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
@UtilityClass
|
||||
public class NotificationPrefix {
|
||||
|
||||
public static final String ALERT_FIRING_PREFIX = "Активно: ";
|
||||
public static final String ALERT_RESOLVED_PREFIX = "Более не активно: ";
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
package dev.vality.alerting.mayday.alertmanager.constant;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
@UtilityClass
|
||||
public class WebhookStatus {
|
||||
public static final String FIRING = "firing";
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
package dev.vality.alerting.mayday.alertmanager.controller;
|
||||
|
||||
import dev.vality.alerting.mayday.alertmanager.model.Webhook;
|
||||
import dev.vality.alerting.mayday.alertmanager.service.AlertmanagerService;
|
||||
import dev.vality.alerting.mayday.common.constant.PrometheusRuleLabel;
|
||||
import dev.vality.alerting.tg_bot.Notification;
|
||||
import dev.vality.alerting.tg_bot.NotifierServiceSrv;
|
||||
import dev.vality.alerting.tg_bot.ReceiverNotFound;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.thrift.TException;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/alertmanager")
|
||||
@RequiredArgsConstructor
|
||||
public class WebhookController {
|
||||
|
||||
private final NotifierServiceSrv.Iface telegramBotClient;
|
||||
private final Converter<Webhook.Alert, Notification> webhookAlertToNotificationConverter;
|
||||
private final AlertmanagerService alertmanagerService;
|
||||
|
||||
@PostMapping(value = "/webhook", consumes = MediaType.APPLICATION_JSON_VALUE)
|
||||
public ResponseEntity<String> processWebhook(@RequestBody Webhook webhook) {
|
||||
log.info("Received webhook from alertmanager: {}", webhook);
|
||||
for (Webhook.Alert alert : webhook.getAlerts()) {
|
||||
try {
|
||||
String userId = alert.getLabels().get(PrometheusRuleLabel.USERNAME);
|
||||
String alertName = alert.getLabels().get(PrometheusRuleLabel.ALERT_NAME);
|
||||
// Алертменеджер может прислать нотификацию уже после того, как пользователь удалил алерт, т.к
|
||||
// обновления в конфигурации применяются не моментально. Поэтому нужна доп.фильтрация здесь.
|
||||
if (alertmanagerService.containsUserRoute(userId, alertName)) {
|
||||
var notification = webhookAlertToNotificationConverter.convert(alert);
|
||||
telegramBotClient.notify(notification);
|
||||
log.info("Alertmanager webhook processed successfully: {}", webhook);
|
||||
}
|
||||
} catch (ReceiverNotFound receiverNotFound) {
|
||||
log.error("Unable to find notification receiver '{}':", webhook.getReceiver(), receiverNotFound);
|
||||
} catch (TException e) {
|
||||
log.error("Unexpected error during notification delivery:", e);
|
||||
return ResponseEntity.internalServerError().build();
|
||||
}
|
||||
}
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package dev.vality.alerting.mayday.alertmanager.converter;
|
||||
|
||||
import dev.vality.alerting.mayday.alertmanager.constant.NotificationPrefix;
|
||||
import dev.vality.alerting.mayday.alertmanager.constant.WebhookStatus;
|
||||
import dev.vality.alerting.mayday.alertmanager.model.Webhook;
|
||||
import dev.vality.alerting.mayday.common.constant.PrometheusRuleAnnotation;
|
||||
import dev.vality.alerting.mayday.common.constant.PrometheusRuleLabel;
|
||||
import dev.vality.alerting.tg_bot.Notification;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@Component
|
||||
public class AlertmanagerWebhookAlertToTelegramBotNotificationsConverter
|
||||
implements Converter<Webhook.Alert, Notification> {
|
||||
@Override
|
||||
public Notification convert(Webhook.Alert alert) {
|
||||
var annotations = alert.getAnnotations();
|
||||
return new Notification()
|
||||
.setId(UUID.randomUUID().toString())
|
||||
.setReceiverId(alert.getLabels().get(PrometheusRuleLabel.USERNAME))
|
||||
.setMessage(createMessage(alert.getStatus().equals(WebhookStatus.FIRING), annotations));
|
||||
}
|
||||
|
||||
private String createMessage(boolean isFiring, Map<String, String> annotations) {
|
||||
String prefix = isFiring ? NotificationPrefix.ALERT_FIRING_PREFIX :
|
||||
NotificationPrefix.ALERT_RESOLVED_PREFIX;
|
||||
return prefix + annotations.get(PrometheusRuleAnnotation.ALERT_DESCRIPTION);
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
package dev.vality.alerting.mayday.alertmanager.model;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Alertmanager webhook body
|
||||
*/
|
||||
@Data
|
||||
public class Webhook {
|
||||
|
||||
private String status;
|
||||
private String receiver;
|
||||
private List<Alert> alerts;
|
||||
|
||||
@Data
|
||||
public static class Alert {
|
||||
|
||||
private String status;
|
||||
private Map<String, String> labels;
|
||||
private Map<String, String> annotations;
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,137 @@
|
||||
package dev.vality.alerting.mayday.alertmanager.service;
|
||||
|
||||
import dev.vality.alerting.mayday.alertmanager.client.k8s.AlertmanagerClient;
|
||||
import dev.vality.alerting.mayday.alertmanager.client.k8s.model.AlertmanagerConfig;
|
||||
import dev.vality.alerting.mayday.alertmanager.client.k8s.model.AlertmanagerConfigSpec;
|
||||
import dev.vality.alerting.mayday.alertmanager.client.k8s.util.AlertmanagerFunctionsUtil;
|
||||
import dev.vality.alerting.mayday.alertmanager.config.properties.AlertmanagerWebhookProperties;
|
||||
import dev.vality.alerting.mayday.alertmanager.config.properties.K8sAlertmanagerProperties;
|
||||
import dev.vality.alerting.mayday.alertmanager.util.FormatUtil;
|
||||
import dev.vality.alerting.mayday.common.constant.AlertConfigurationRequiredParameter;
|
||||
import dev.vality.alerting.mayday.common.constant.PrometheusRuleLabel;
|
||||
import dev.vality.alerting.mayday.common.dto.CreateAlertDto;
|
||||
import io.fabric8.kubernetes.api.model.ObjectMeta;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AlertmanagerService {
|
||||
|
||||
private static final String ONE_SEC_WAIT = FormatUtil.formatSecondsDuration("1");
|
||||
|
||||
private final AlertmanagerWebhookProperties alertmanagerWebhookProperties;
|
||||
private final K8sAlertmanagerProperties k8sAlertmanagerProperties;
|
||||
private final AlertmanagerClient alertmanagerClient;
|
||||
@Value("${spring.application.name}")
|
||||
private String applicationName;
|
||||
private String alertmanagerConfigName;
|
||||
|
||||
public void createUserRoute(CreateAlertDto createAlertDto) {
|
||||
if (alertmanagerClient.getAlertmanagerConfig(getAlertmanagerConfigName()).isEmpty()) {
|
||||
log.info("Alertmanager config '{}' not found and will be created", getAlertmanagerConfigName());
|
||||
alertmanagerClient.createAlertmanagerConfig(buildAlertmanagerConfig());
|
||||
log.info("Alertmanager config '{}' was created successfully", getAlertmanagerConfigName());
|
||||
}
|
||||
alertmanagerClient.addRouteIfNotExists(getAlertmanagerConfigName(), buildRoute(createAlertDto));
|
||||
}
|
||||
|
||||
public String getAlertmanagerConfigName() {
|
||||
if (ObjectUtils.isEmpty(alertmanagerConfigName)) {
|
||||
alertmanagerConfigName = "%s-managed-rule".formatted(applicationName);
|
||||
}
|
||||
return alertmanagerConfigName;
|
||||
}
|
||||
|
||||
private AlertmanagerConfig buildAlertmanagerConfig() {
|
||||
var webhookConfig = buildWebhookConfig();
|
||||
var receiver = buildAlertManagerConfigReceiver(webhookConfig);
|
||||
var rootRoute = buildAlertmanagerConfigRootRoute();
|
||||
var spec = buildAlertmanagerConfigSpec(receiver, rootRoute);
|
||||
AlertmanagerConfig alertmanagerConfig = new AlertmanagerConfig();
|
||||
alertmanagerConfig.setSpec(spec);
|
||||
alertmanagerConfig.setMetadata(buildAlertmanagerConfigMetadata());
|
||||
return alertmanagerConfig;
|
||||
}
|
||||
|
||||
private AlertmanagerConfigSpec.ChildRoute buildRoute(CreateAlertDto createAlertDto) {
|
||||
AlertmanagerConfigSpec.ChildRoute route = new AlertmanagerConfigSpec.ChildRoute();
|
||||
route.setReceiver(applicationName);
|
||||
route.setGroupBy(Set.of(PrometheusRuleLabel.ALERT_NAME));
|
||||
route.setGroupWait(ONE_SEC_WAIT);
|
||||
route.setGroupInterval(ONE_SEC_WAIT);
|
||||
var alertnameMatcher =
|
||||
AlertmanagerFunctionsUtil.createMatcher(PrometheusRuleLabel.ALERT_NAME, createAlertDto.getAlertId());
|
||||
var usernameMatcher =
|
||||
AlertmanagerFunctionsUtil.createMatcher(PrometheusRuleLabel.USERNAME, createAlertDto.getUserId());
|
||||
route.setMatchers(Set.of(alertnameMatcher, usernameMatcher));
|
||||
route.setRepeatInterval(FormatUtil.formatMinutesDuration(createAlertDto.getParameters()
|
||||
.get(String.valueOf(
|
||||
AlertConfigurationRequiredParameter.ALERT_REPEAT_MINUTES.getSubstitutionName())).get(0)));
|
||||
return route;
|
||||
}
|
||||
|
||||
private AlertmanagerConfigSpec.WebhookConfig buildWebhookConfig() {
|
||||
var webhookConfig = new AlertmanagerConfigSpec.WebhookConfig();
|
||||
webhookConfig.setUrl(alertmanagerWebhookProperties.getUrl() + alertmanagerWebhookProperties.getPath());
|
||||
return webhookConfig;
|
||||
}
|
||||
|
||||
private AlertmanagerConfigSpec.Receiver buildAlertManagerConfigReceiver(
|
||||
AlertmanagerConfigSpec.WebhookConfig webhookConfig) {
|
||||
AlertmanagerConfigSpec.Receiver receiver = new AlertmanagerConfigSpec.Receiver();
|
||||
receiver.setName(applicationName);
|
||||
receiver.setWebhookConfigs(Set.of(webhookConfig));
|
||||
return receiver;
|
||||
}
|
||||
|
||||
private AlertmanagerConfigSpec.Route buildAlertmanagerConfigRootRoute() {
|
||||
AlertmanagerConfigSpec.Route rootRoute = new AlertmanagerConfigSpec.Route();
|
||||
rootRoute.setReceiver(applicationName);
|
||||
rootRoute.setMatchers(Set.of(AlertmanagerFunctionsUtil.createMatcher(PrometheusRuleLabel.SERVICE,
|
||||
applicationName)));
|
||||
return rootRoute;
|
||||
}
|
||||
|
||||
private AlertmanagerConfigSpec buildAlertmanagerConfigSpec(AlertmanagerConfigSpec.Receiver receiver,
|
||||
AlertmanagerConfigSpec.Route route) {
|
||||
AlertmanagerConfigSpec spec = new AlertmanagerConfigSpec();
|
||||
spec.setReceivers(Set.of(receiver));
|
||||
spec.setRoute(route);
|
||||
return spec;
|
||||
}
|
||||
|
||||
private ObjectMeta buildAlertmanagerConfigMetadata() {
|
||||
var metadata = new ObjectMeta();
|
||||
metadata.setLabels(k8sAlertmanagerProperties.getLabels());
|
||||
metadata.setName(getAlertmanagerConfigName());
|
||||
return metadata;
|
||||
}
|
||||
|
||||
public void deleteUserRoute(String alertId) {
|
||||
if (alertmanagerClient.getAlertmanagerConfig(getAlertmanagerConfigName()).isEmpty()) {
|
||||
log.warn("Alertmanager config '{}' not found, no need to delete user route", getAlertmanagerConfigName());
|
||||
return;
|
||||
}
|
||||
alertmanagerClient.deleteRoute(getAlertmanagerConfigName(), alertId);
|
||||
}
|
||||
|
||||
public void deleteUserRoutes(String userId) {
|
||||
if (alertmanagerClient.getAlertmanagerConfig(getAlertmanagerConfigName()).isEmpty()) {
|
||||
log.warn("Alertmanager config '{}' not found, no need to delete user route", getAlertmanagerConfigName());
|
||||
return;
|
||||
}
|
||||
alertmanagerClient.deleteRoutes(getAlertmanagerConfigName(), userId);
|
||||
}
|
||||
|
||||
public boolean containsUserRoute(String userId, String alertName) {
|
||||
return alertmanagerClient.containsRoute(getAlertmanagerConfigName(), userId, alertName);
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package dev.vality.alerting.mayday.alertmanager.util;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@UtilityClass
|
||||
public class FormatUtil {
|
||||
|
||||
public static String formatMinutesDuration(String value) {
|
||||
return formatDuration(value, TimeUnit.MINUTES);
|
||||
}
|
||||
|
||||
public static String formatSecondsDuration(String value) {
|
||||
return formatDuration(value, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
public static String formatDuration(String value, TimeUnit timeUnit) {
|
||||
String unit = switch (timeUnit) {
|
||||
case MILLISECONDS -> "ms";
|
||||
case SECONDS -> "s";
|
||||
case MINUTES -> "m";
|
||||
case HOURS -> "h";
|
||||
case DAYS -> "d";
|
||||
default -> throw new IllegalArgumentException(timeUnit + " is not supported!");
|
||||
};
|
||||
return value + unit;
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
package dev.vality.alerting.mayday.alerttemplate.config;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import dev.vality.alerting.mayday.alerttemplate.error.AlertConfigurationException;
|
||||
import dev.vality.alerting.mayday.alerttemplate.model.alerttemplate.AlertTemplate;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.support.ResourcePatternResolver;
|
||||
|
||||
import javax.validation.ConstraintViolation;
|
||||
import javax.validation.ConstraintViolationException;
|
||||
import javax.validation.Validator;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Configuration
|
||||
public class AlertConfigurationsConfig {
|
||||
|
||||
|
||||
@Bean
|
||||
public Map<String, AlertTemplate> alertConfigurations(ResourcePatternResolver resourcePatternResolver,
|
||||
ObjectMapper objectMapper,
|
||||
Validator validator)
|
||||
throws IOException {
|
||||
Resource[] resources = resourcePatternResolver.getResources("classpath:template/*.json");
|
||||
log.info("Found {} supported alert configurations", resources.length);
|
||||
Map<String, AlertTemplate> templateMap = Arrays.stream(resources)
|
||||
.map(resource -> {
|
||||
try {
|
||||
return objectMapper.readValue(resource.getURL(), AlertTemplate.class);
|
||||
} catch (IOException e) {
|
||||
throw new AlertConfigurationException("Unable to parse alert configuration: " +
|
||||
resource.getFilename(), e);
|
||||
}
|
||||
}).collect(Collectors.toMap(AlertTemplate::getId, alertTemplate -> alertTemplate));
|
||||
|
||||
Set<ConstraintViolation<AlertTemplate>> violations =
|
||||
templateMap.entrySet().stream().flatMap(entry -> validator.validate(entry.getValue()).stream())
|
||||
.collect(Collectors.toSet());
|
||||
if (!violations.isEmpty()) {
|
||||
throw new ConstraintViolationException(violations);
|
||||
}
|
||||
return templateMap;
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
package dev.vality.alerting.mayday.alerttemplate.dao;
|
||||
|
||||
import dev.vality.alerting.mayday.alerttemplate.model.daway.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface DawayDao {
|
||||
|
||||
List<Terminal> getAllTerminals();
|
||||
|
||||
List<Terminal> getPaymentTerminals();
|
||||
|
||||
List<Terminal> getPayoutTerminals();
|
||||
|
||||
List<Provider> getAllProviders();
|
||||
|
||||
List<Provider> getPaymentProviders();
|
||||
|
||||
List<Provider> getPayoutProviders();
|
||||
|
||||
List<Shop> getShops();
|
||||
|
||||
List<Wallet> getWallets();
|
||||
|
||||
List<Currency> getCurrencies();
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
package dev.vality.alerting.mayday.alerttemplate.dao.impl;
|
||||
|
||||
import dev.vality.alerting.mayday.alerttemplate.dao.DawayDao;
|
||||
import dev.vality.alerting.mayday.alerttemplate.model.daway.*;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
@RequiredArgsConstructor
|
||||
public class DawayDaoImpl implements DawayDao {
|
||||
|
||||
private final RowMapper<Provider> providerRowMapper;
|
||||
private final RowMapper<Terminal> terminalRowMapper;
|
||||
private final RowMapper<Shop> shopRowMapper;
|
||||
private final RowMapper<Wallet> walletRowMapper;
|
||||
private final RowMapper<Currency> currencyRowMapper;
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
|
||||
@Override
|
||||
public List<Terminal> getAllTerminals() {
|
||||
return jdbcTemplate
|
||||
.query("select terminal_ref_id, name from dw.terminal where current = true", terminalRowMapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Terminal> getPaymentTerminals() {
|
||||
return jdbcTemplate
|
||||
.query("select t.terminal_ref_id, t.name from dw.terminal as t inner join dw.provider as p on t" +
|
||||
".terminal_provider_ref_id = p.provider_ref_id and p.current and p.payment_terms_json is not " +
|
||||
"null where t.current", terminalRowMapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Terminal> getPayoutTerminals() {
|
||||
return jdbcTemplate
|
||||
.query("select t.terminal_ref_id, t.name from dw.terminal as t inner join dw.provider as p on t" +
|
||||
".terminal_provider_ref_id = p.provider_ref_id and p.current and p.wallet_terms_json is not " +
|
||||
"null where t.current", terminalRowMapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Provider> getAllProviders() {
|
||||
return jdbcTemplate
|
||||
.query("select provider_ref_id, name from dw.provider where current = true", providerRowMapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Provider> getPaymentProviders() {
|
||||
return jdbcTemplate
|
||||
.query("select provider_ref_id, name from dw.provider where current = true and payment_terms_json is " +
|
||||
"not null", providerRowMapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Provider> getPayoutProviders() {
|
||||
return jdbcTemplate
|
||||
.query("select provider_ref_id, name from dw.provider where current = true and wallet_terms_json is " +
|
||||
"not null", providerRowMapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Shop> getShops() {
|
||||
return jdbcTemplate
|
||||
.query("select distinct (shop_id), details_name from dw.shop where current = true", shopRowMapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Wallet> getWallets() {
|
||||
return jdbcTemplate
|
||||
.query("select wallet_id, wallet_name from dw.wallet where current = true", walletRowMapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Currency> getCurrencies() {
|
||||
return jdbcTemplate
|
||||
.query("select symbolic_code, name from dw.currency where current = true", currencyRowMapper);
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package dev.vality.alerting.mayday.alerttemplate.dao.impl.mapper;
|
||||
|
||||
import dev.vality.alerting.mayday.alerttemplate.model.daway.Currency;
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
@Component
|
||||
public class CurrencyRowMapper implements RowMapper<Currency> {
|
||||
@Override
|
||||
public Currency mapRow(ResultSet rs, int rowNum) throws SQLException {
|
||||
return Currency.builder()
|
||||
.symbolicCode(rs.getString("symbolic_code"))
|
||||
.name(rs.getString("name"))
|
||||
.build();
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package dev.vality.alerting.mayday.alerttemplate.dao.impl.mapper;
|
||||
|
||||
import dev.vality.alerting.mayday.alerttemplate.model.daway.Provider;
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
@Component
|
||||
public class ProviderRowMapper implements RowMapper<Provider> {
|
||||
@Override
|
||||
public Provider mapRow(ResultSet rs, int rowNum) throws SQLException {
|
||||
return Provider.builder()
|
||||
.id(rs.getInt("provider_ref_id"))
|
||||
.name(rs.getString("name"))
|
||||
.build();
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package dev.vality.alerting.mayday.alerttemplate.dao.impl.mapper;
|
||||
|
||||
import dev.vality.alerting.mayday.alerttemplate.model.daway.Shop;
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
@Component
|
||||
public class ShopRowMapper implements RowMapper<Shop> {
|
||||
@Override
|
||||
public Shop mapRow(ResultSet rs, int rowNum) throws SQLException {
|
||||
return Shop.builder()
|
||||
.id(rs.getString("shop_id"))
|
||||
.name(rs.getString("details_name"))
|
||||
.build();
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package dev.vality.alerting.mayday.alerttemplate.dao.impl.mapper;
|
||||
|
||||
import dev.vality.alerting.mayday.alerttemplate.model.daway.Terminal;
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
@Component
|
||||
public class TerminalRowMapper implements RowMapper<Terminal> {
|
||||
@Override
|
||||
public Terminal mapRow(ResultSet rs, int rowNum) throws SQLException {
|
||||
return Terminal.builder()
|
||||
.id(rs.getInt("terminal_ref_id"))
|
||||
.name(rs.getString("name"))
|
||||
.build();
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package dev.vality.alerting.mayday.alerttemplate.dao.impl.mapper;
|
||||
|
||||
import dev.vality.alerting.mayday.alerttemplate.model.daway.Wallet;
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
@Component
|
||||
public class WalletRowMapper implements RowMapper<Wallet> {
|
||||
@Override
|
||||
public Wallet mapRow(ResultSet rs, int rowNum) throws SQLException {
|
||||
return Wallet.builder()
|
||||
.id(rs.getString("wallet_id"))
|
||||
.name(rs.getString("wallet_name"))
|
||||
.build();
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package dev.vality.alerting.mayday.alerttemplate.error;
|
||||
|
||||
public class AlertConfigurationException extends RuntimeException {
|
||||
|
||||
public AlertConfigurationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public AlertConfigurationException(String message, Throwable e) {
|
||||
super(message, e);
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package dev.vality.alerting.mayday.alerttemplate.error;
|
||||
|
||||
public class AlertTemplateNotFoundException extends RuntimeException {
|
||||
|
||||
public AlertTemplateNotFoundException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
package dev.vality.alerting.mayday.alerttemplate.model.alerttemplate;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class AlertTemplate {
|
||||
|
||||
@NotNull
|
||||
private final String id;
|
||||
@NotNull
|
||||
@JsonProperty("readable_name")
|
||||
private final String readableName;
|
||||
@NotNull
|
||||
@JsonProperty("prometheus_query")
|
||||
private final String prometheusQuery;
|
||||
@NotNull
|
||||
@JsonProperty("alert_name_template")
|
||||
private final String alertNameTemplate;
|
||||
@NotNull
|
||||
@JsonProperty("alert_notification_template")
|
||||
private final String alertNotificationTemplate;
|
||||
private List<AlertConfigurationParameter> parameters;
|
||||
|
||||
@Data
|
||||
public static class AlertConfigurationParameter {
|
||||
@NotNull
|
||||
private final Integer id;
|
||||
@NotNull
|
||||
@JsonProperty("substitution_name")
|
||||
private final String substitutionName;
|
||||
@NotNull
|
||||
@JsonProperty("readable_name")
|
||||
private final String readableName;
|
||||
@NotNull
|
||||
private final Boolean mandatory;
|
||||
@JsonProperty(value = "multiple_values")
|
||||
private final Boolean multipleValues = false;
|
||||
@JsonProperty("dictionary_name")
|
||||
private final DictionaryType dictionaryName;
|
||||
private final String regexp;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
package dev.vality.alerting.mayday.alerttemplate.model.alerttemplate;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public enum DictionaryType {
|
||||
|
||||
@JsonProperty("terminals")
|
||||
TERMINALS,
|
||||
@JsonProperty("payment_terminals")
|
||||
PAYMENT_TERMINALS,
|
||||
@JsonProperty("payout_terminals")
|
||||
PAYOUT_TERMINALS,
|
||||
@JsonProperty("providers")
|
||||
PROVIDERS,
|
||||
@JsonProperty("payment_providers")
|
||||
PAYMENT_PROVIDERS,
|
||||
@JsonProperty("payout_providers")
|
||||
PAYOUT_PROVIDERS,
|
||||
@JsonProperty("wallets")
|
||||
WALLETS,
|
||||
@JsonProperty("shops")
|
||||
SHOPS,
|
||||
@JsonProperty("payment_limit_scopes")
|
||||
PAYMENT_LIMIT_SCOPES,
|
||||
@JsonProperty("payout_limit_scopes")
|
||||
PAYOUT_LIMIT_SCOPES,
|
||||
@JsonProperty("boundaries")
|
||||
CONDITIONAL_BOUNDARIES,
|
||||
@JsonProperty("currencies")
|
||||
CURRENCIES,
|
||||
@JsonProperty("time_interval_boundaries")
|
||||
TIME_INTERVAL_BOUNDARIES,
|
||||
@JsonProperty("aggregation_intervals")
|
||||
AGGREGATION_INTERVALS;
|
||||
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package dev.vality.alerting.mayday.alerttemplate.model.daway;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class Currency {
|
||||
private String symbolicCode;
|
||||
private String name;
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package dev.vality.alerting.mayday.alerttemplate.model.daway;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class Provider {
|
||||
private Integer id;
|
||||
private String name;
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package dev.vality.alerting.mayday.alerttemplate.model.daway;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class Shop {
|
||||
private String id;
|
||||
private String name;
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package dev.vality.alerting.mayday.alerttemplate.model.daway;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class Terminal {
|
||||
private Integer id;
|
||||
private String name;
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package dev.vality.alerting.mayday.alerttemplate.model.daway;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class Wallet {
|
||||
private String id;
|
||||
private String name;
|
||||
}
|
@ -0,0 +1,100 @@
|
||||
package dev.vality.alerting.mayday.alerttemplate.service;
|
||||
|
||||
import dev.vality.alerting.mayday.alerttemplate.dao.DawayDao;
|
||||
import dev.vality.alerting.mayday.alerttemplate.model.alerttemplate.DictionaryType;
|
||||
import dev.vality.alerting.mayday.alerttemplate.model.daway.*;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class DictionaryService {
|
||||
|
||||
private final DawayDao dawayDao;
|
||||
|
||||
public Map<String, String> getDictionary(DictionaryType type) {
|
||||
return switch (type) {
|
||||
case TERMINALS -> convertTerminalsToDictionary(dawayDao.getAllTerminals());
|
||||
case PAYMENT_TERMINALS -> convertTerminalsToDictionary(dawayDao.getPaymentTerminals());
|
||||
case PAYOUT_TERMINALS -> convertTerminalsToDictionary(dawayDao.getPayoutTerminals());
|
||||
case PROVIDERS -> convertProvidersToDictionary(dawayDao.getAllProviders());
|
||||
case PAYMENT_PROVIDERS -> convertProvidersToDictionary(dawayDao.getPaymentProviders());
|
||||
case PAYOUT_PROVIDERS -> convertProvidersToDictionary(dawayDao.getPayoutProviders());
|
||||
case WALLETS -> convertWalletsToDictionary(dawayDao.getWallets());
|
||||
case SHOPS -> convertShopsToDictionary(dawayDao.getShops());
|
||||
case CURRENCIES -> convertCurrenciesToDictionary(dawayDao.getCurrencies());
|
||||
case PAYMENT_LIMIT_SCOPES -> Map.of("Провайдер", "provider",
|
||||
"Провайдер + терминал", "provider,terminal",
|
||||
"Провайдер + терминал + магазин", "provider,shop,terminal",
|
||||
"Терминал", "terminal",
|
||||
"Магазин", "shop");
|
||||
case PAYOUT_LIMIT_SCOPES -> Map.of("Провайдер", "provider",
|
||||
"Провайдер + терминал", "provider,terminal",
|
||||
"Провайдер + терминал + кошелек", "provider,terminal,wallet",
|
||||
"Терминал", "terminal",
|
||||
"Кошелёк", "wallet");
|
||||
case CONDITIONAL_BOUNDARIES -> Map.of("Больше порогового значения", ">", "Меньше порогового значения", "<");
|
||||
case TIME_INTERVAL_BOUNDARIES -> Map.of("Да", "unless", "Нет", "and");
|
||||
case AGGREGATION_INTERVALS -> Map.of("5 минут", "5m", "15 минут", "15m", "30 минут", "30m",
|
||||
"1 час", "1h", "3 часа", "3h", "6 часов", "6h", "12 часов", "12h", "24 часа", "24h");
|
||||
};
|
||||
}
|
||||
|
||||
private Map<String, String> convertTerminalsToDictionary(List<Terminal> terminals) {
|
||||
return terminals.stream()
|
||||
.collect(Collectors.toMap(
|
||||
terminal -> formatDictionaryKey(Integer.toString(terminal.getId()), terminal.getName()),
|
||||
terminal -> Integer.toString(terminal.getId())));
|
||||
}
|
||||
|
||||
private Map<String, String> convertProvidersToDictionary(List<Provider> providers) {
|
||||
return providers.stream()
|
||||
.collect(Collectors.toMap(
|
||||
provider -> formatDictionaryKey(Integer.toString(provider.getId()), provider.getName()),
|
||||
provider -> Integer.toString(provider.getId())));
|
||||
}
|
||||
|
||||
private Map<String, String> convertWalletsToDictionary(List<Wallet> wallets) {
|
||||
return wallets.stream()
|
||||
.collect(Collectors.toMap(
|
||||
wallet -> formatDictionaryKey(wallet.getId(), wallet.getName()),
|
||||
Wallet::getId));
|
||||
}
|
||||
|
||||
private Map<String, String> convertShopsToDictionary(List<Shop> shops) {
|
||||
return shops.stream()
|
||||
.collect(Collectors.toMap(
|
||||
shop -> formatDictionaryKey(formatShopId(shop.getId()), shop.getName()),
|
||||
Shop::getId));
|
||||
}
|
||||
|
||||
// Возвращаем только часть UUID, т.к иначе строка выходит слишком длинной
|
||||
private String formatShopId(String shopId) {
|
||||
try {
|
||||
UUID.fromString(shopId);
|
||||
return shopId.substring(0, shopId.indexOf("-"));
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.warn("Unable to format shopId '{}'", shopId);
|
||||
return shopId;
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, String> convertCurrenciesToDictionary(List<Currency> currencies) {
|
||||
return currencies.stream()
|
||||
.collect(Collectors.toMap(
|
||||
currency -> formatDictionaryKey(currency.getSymbolicCode(), currency.getName()),
|
||||
Currency::getSymbolicCode));
|
||||
}
|
||||
|
||||
private String formatDictionaryKey(String id, String description) {
|
||||
return String.format("(%s) %s", id, description);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package dev.vality.alerting.mayday.alerttemplate.service;
|
||||
|
||||
import dev.vality.alerting.mayday.alerttemplate.error.AlertTemplateNotFoundException;
|
||||
import dev.vality.alerting.mayday.alerttemplate.model.alerttemplate.AlertTemplate;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class TemplateService {
|
||||
|
||||
private final Map<String, AlertTemplate> alertConfigurations;
|
||||
|
||||
public AlertTemplate getAlertTemplateById(String metricTemplateId) {
|
||||
return alertConfigurations.get(metricTemplateId);
|
||||
}
|
||||
|
||||
public List<AlertTemplate> getAlertTemplates() {
|
||||
return new ArrayList<>(alertConfigurations.values());
|
||||
}
|
||||
|
||||
public List<AlertTemplate.AlertConfigurationParameter> getAlertTemplateParams(String templateId) {
|
||||
if (!alertConfigurations.containsKey(templateId)) {
|
||||
throw new AlertTemplateNotFoundException(String.format("Unable to find templateId '%s'", templateId));
|
||||
}
|
||||
return alertConfigurations.get(templateId).getParameters();
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,186 @@
|
||||
package dev.vality.alerting.mayday.alerttemplate.service.helper;
|
||||
|
||||
import dev.vality.alerting.mayday.CreateAlertRequest;
|
||||
import dev.vality.alerting.mayday.ParameterInfo;
|
||||
import dev.vality.alerting.mayday.alerttemplate.error.AlertConfigurationException;
|
||||
import dev.vality.alerting.mayday.alerttemplate.model.alerttemplate.AlertTemplate;
|
||||
import dev.vality.alerting.mayday.alerttemplate.service.DictionaryService;
|
||||
import dev.vality.alerting.mayday.common.constant.AlertConfigurationRequiredParameter;
|
||||
import dev.vality.alerting.mayday.common.dto.CreateAlertDto;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.DigestUtils;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class TemplateHelper {
|
||||
|
||||
private static final String anyClientValue = "-";
|
||||
private static final String anyPrometheusValue = ".*";
|
||||
private static final String anyUserFriendlyValue = "<любое значение>";
|
||||
private static final String multiValuePrometheusDelimiter = "|";
|
||||
private static final String multiValueUserFriendlyDelimiter = ",";
|
||||
|
||||
private final DictionaryService dictionaryService;
|
||||
|
||||
public CreateAlertDto preparePrometheusRuleData(CreateAlertRequest createAlertRequest,
|
||||
AlertTemplate metricTemplate,
|
||||
List<AlertTemplate.AlertConfigurationParameter>
|
||||
metricParams) {
|
||||
|
||||
Map<String, List<String>> parameters = mergeParameters(createAlertRequest.getParameters(), metricParams);
|
||||
String queryExpression = prepareMetricExpression(metricTemplate, parameters);
|
||||
log.debug("Prepared prometheus expression: {}", queryExpression);
|
||||
String alertId = generateAlertId(createAlertRequest, queryExpression);
|
||||
|
||||
return CreateAlertDto.builder()
|
||||
.alertId(alertId)
|
||||
.prometheusQuery(queryExpression)
|
||||
.userId(createAlertRequest.getUserId())
|
||||
.userFriendlyAlertName(prepareUserFriendlyAlertName(metricTemplate, parameters))
|
||||
.userFriendlyAlertDescription(prepareMetricAlertMessage(metricTemplate, parameters))
|
||||
.parameters(parameters)
|
||||
.build();
|
||||
}
|
||||
|
||||
protected Map<String, List<String>> mergeParameters(List<ParameterInfo> externalParamsInfo,
|
||||
List<AlertTemplate.AlertConfigurationParameter>
|
||||
maydayParamsInfo) {
|
||||
Map<String, List<String>> params = maydayParamsInfo.stream()
|
||||
.map(maydayParamInfo -> {
|
||||
var externalParamInfos = externalParamsInfo.stream()
|
||||
.filter(userParamInfo ->
|
||||
maydayParamInfo.getId().toString().equals(userParamInfo.getId()))
|
||||
.toList();
|
||||
|
||||
validateMultipleValues(maydayParamInfo, externalParamInfos);
|
||||
validateMandatoryValues(maydayParamInfo, externalParamInfos);
|
||||
|
||||
List<String> values = new ArrayList<>();
|
||||
if (!maydayParamInfo.getMandatory() && externalParamInfos.size() == 1) {
|
||||
values.add(hasNoValue(externalParamInfos.get(0)) ? anyPrometheusValue :
|
||||
getParameterValue(maydayParamInfo, externalParamInfos.get(0)));
|
||||
} else {
|
||||
externalParamInfos.stream()
|
||||
.filter(parameterInfo -> !hasNoValue(parameterInfo))
|
||||
.map(parameterInfo -> getParameterValue(maydayParamInfo,
|
||||
parameterInfo))
|
||||
.forEach(values::add);
|
||||
}
|
||||
return Map.of(maydayParamInfo.getSubstitutionName(),
|
||||
values);
|
||||
}
|
||||
).flatMap(map -> map.entrySet().stream())
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
|
||||
// Add required parameters
|
||||
Arrays.stream(AlertConfigurationRequiredParameter.values()).forEach(
|
||||
requiredParameter -> {
|
||||
var param = getRequiredParameter(String.valueOf(requiredParameter.getId()), externalParamsInfo);
|
||||
params.put(requiredParameter.getSubstitutionName(), List.of(param.getValue()));
|
||||
}
|
||||
);
|
||||
return params;
|
||||
}
|
||||
|
||||
private void validateMultipleValues(AlertTemplate.AlertConfigurationParameter maydayParamInfo,
|
||||
List<ParameterInfo> externalParamInfos) {
|
||||
if (externalParamInfos.size() > 1 && !maydayParamInfo.getMultipleValues()) {
|
||||
throw new AlertConfigurationException(String.format("Parameter '%s' cannot have " +
|
||||
"multiple values!", maydayParamInfo.getSubstitutionName()));
|
||||
}
|
||||
}
|
||||
|
||||
private void validateMandatoryValues(AlertTemplate.AlertConfigurationParameter maydayParamInfo,
|
||||
List<ParameterInfo> externalParamInfos) {
|
||||
if ((externalParamInfos.isEmpty() || externalParamInfos.size() == 1
|
||||
&& hasNoValue(externalParamInfos.get(0))) && maydayParamInfo.getMandatory()) {
|
||||
throw new AlertConfigurationException("Unable to find required" +
|
||||
" parameter: " + maydayParamInfo.getSubstitutionName());
|
||||
}
|
||||
}
|
||||
|
||||
protected ParameterInfo getRequiredParameter(String name, List<ParameterInfo> parameterInfos) {
|
||||
return parameterInfos.stream()
|
||||
.filter(paramInfo ->
|
||||
paramInfo.getId().equals(name))
|
||||
.findFirst().orElseThrow(() -> new AlertConfigurationException("Unable to find required" +
|
||||
" parameter: " + name));
|
||||
}
|
||||
|
||||
protected String generateAlertId(CreateAlertRequest createAlertRequest, String preparedExpression) {
|
||||
return DigestUtils.md5DigestAsHex((createAlertRequest.getUserId() + preparedExpression)
|
||||
.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
protected String prepareMetricExpression(AlertTemplate metricTemplate, Map<String, List<String>> parameters) {
|
||||
return prepareTemplate(metricTemplate.getPrometheusQuery(), parameters);
|
||||
}
|
||||
|
||||
protected String prepareUserFriendlyAlertName(AlertTemplate metricTemplate, Map<String, List<String>> parameters) {
|
||||
return prepareUserFriendlyTemplate(metricTemplate.getAlertNameTemplate(), parameters);
|
||||
}
|
||||
|
||||
protected String prepareMetricAlertMessage(AlertTemplate metricTemplate, Map<String, List<String>> parameters) {
|
||||
return prepareUserFriendlyTemplate(metricTemplate.getAlertNotificationTemplate(), parameters);
|
||||
}
|
||||
|
||||
private String prepareTemplate(String template, Map<String, List<String>> replacements) {
|
||||
String preparedTemplate = template;
|
||||
var replacementsEntries = replacements.entrySet();
|
||||
for (Map.Entry<String, List<String>> entry : replacementsEntries) {
|
||||
String value = entry.getValue().size() == 1
|
||||
? entry.getValue().get(0) : String.join(multiValuePrometheusDelimiter, entry.getValue());
|
||||
preparedTemplate = preparedTemplate.replace(formatReplacementVariable(entry.getKey()), value);
|
||||
}
|
||||
return preparedTemplate;
|
||||
}
|
||||
|
||||
private String prepareUserFriendlyTemplate(String template, Map<String, List<String>> replacements) {
|
||||
String preparedTemplate = template;
|
||||
var replacementsEntries = replacements.entrySet();
|
||||
for (Map.Entry<String, List<String>> entry : replacementsEntries) {
|
||||
String value = entry.getValue().size() == 1
|
||||
? formatAnyValue(entry.getValue().get(0)) : String.join(multiValueUserFriendlyDelimiter,
|
||||
entry.getValue());
|
||||
preparedTemplate = preparedTemplate.replace(formatReplacementVariable(entry.getKey()), value);
|
||||
}
|
||||
return preparedTemplate;
|
||||
}
|
||||
|
||||
private String formatAnyValue(String value) {
|
||||
if (anyPrometheusValue.equals(value)) {
|
||||
return anyUserFriendlyValue;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private String getParameterValue(AlertTemplate.AlertConfigurationParameter maydayParamInfo,
|
||||
ParameterInfo userParamInfo) {
|
||||
if (maydayParamInfo.getDictionaryName() != null) {
|
||||
return dictionaryService.getDictionary(maydayParamInfo.getDictionaryName()).get(userParamInfo.getValue());
|
||||
}
|
||||
return userParamInfo.getValue();
|
||||
}
|
||||
|
||||
private String formatReplacementVariable(String variableName) {
|
||||
return "${" + variableName + "}";
|
||||
}
|
||||
|
||||
private String formatDuration(String durationInMinutes) {
|
||||
return durationInMinutes + "m";
|
||||
}
|
||||
|
||||
private boolean hasNoValue(ParameterInfo parameterInfo) {
|
||||
return anyClientValue.equals(parameterInfo.getValue());
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package dev.vality.alerting.mayday.common.config;
|
||||
|
||||
import io.fabric8.kubernetes.client.Config;
|
||||
import io.fabric8.kubernetes.client.ConfigBuilder;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
public class KubernetesConfig {
|
||||
|
||||
@Value("${k8s.namespace}")
|
||||
private String k8sNamespace;
|
||||
|
||||
@Bean
|
||||
public Config k8sConfig() {
|
||||
if (ObjectUtils.isEmpty(k8sNamespace)) {
|
||||
return new ConfigBuilder().withDefaultNamespace().build();
|
||||
}
|
||||
|
||||
return new ConfigBuilder().withNamespace(k8sNamespace).build();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
package dev.vality.alerting.mayday.common.constant;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
* Обязательные параметры для создания каждого алерта
|
||||
*/
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum AlertConfigurationRequiredParameter {
|
||||
|
||||
ALERT_REPEAT_MINUTES(Integer.MAX_VALUE - 1, "alert_repeat_minutes", "Как часто присылать повторные уведомления (в" +
|
||||
" минутах)?",
|
||||
"^\\d" +
|
||||
"+$");
|
||||
|
||||
private final int id;
|
||||
private final String substitutionName;
|
||||
private final String readableName;
|
||||
private final String regexp;
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
package dev.vality.alerting.mayday.common.constant;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
@UtilityClass
|
||||
public class PrometheusRuleAnnotation {
|
||||
|
||||
public static final String ALERT_NAME = "alertname";
|
||||
public static final String ALERT_DESCRIPTION = "alert_description";
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package dev.vality.alerting.mayday.common.constant;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
@UtilityClass
|
||||
public class PrometheusRuleLabel {
|
||||
|
||||
public static final String USERNAME = "username";
|
||||
public static final String ALERT_NAME = "alertname";
|
||||
public static final String SERVICE = "service";
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package dev.vality.alerting.mayday.common.dto;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class CreateAlertDto {
|
||||
private String alertId;
|
||||
private String userId;
|
||||
private String prometheusQuery;
|
||||
private String userFriendlyAlertName;
|
||||
private String userFriendlyAlertDescription;
|
||||
private Map<String, List<String>> parameters;
|
||||
}
|
@ -0,0 +1,101 @@
|
||||
package dev.vality.alerting.mayday.prometheus.client.k8s;
|
||||
|
||||
import dev.vality.alerting.mayday.prometheus.client.k8s.model.PrometheusRule;
|
||||
import dev.vality.alerting.mayday.prometheus.client.k8s.model.PrometheusRuleSpec;
|
||||
import dev.vality.alerting.mayday.prometheus.client.k8s.util.PrometheusFunctionsUtil;
|
||||
import io.fabric8.kubernetes.api.model.KubernetesResourceList;
|
||||
import io.fabric8.kubernetes.client.Config;
|
||||
import io.fabric8.kubernetes.client.KubernetesClient;
|
||||
import io.fabric8.kubernetes.client.KubernetesClientBuilder;
|
||||
import io.fabric8.kubernetes.client.KubernetesClientException;
|
||||
import io.fabric8.kubernetes.client.dsl.MixedOperation;
|
||||
import io.fabric8.kubernetes.client.dsl.Resource;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class PrometheusClient {
|
||||
|
||||
private final Config k8sConfig;
|
||||
|
||||
public void createPrometheusRule(PrometheusRule prometheusRule) throws KubernetesClientException {
|
||||
try (KubernetesClient client = new KubernetesClientBuilder().withConfig(k8sConfig).build()) {
|
||||
MixedOperation<PrometheusRule, KubernetesResourceList<PrometheusRule>, Resource<PrometheusRule>>
|
||||
prometheusRuleClient = client.resources(PrometheusRule.class);
|
||||
try {
|
||||
prometheusRuleClient.inNamespace(client.getNamespace()).resource(prometheusRule).create();
|
||||
} catch (KubernetesClientException e) {
|
||||
// 409 http код возникает при попытке создать уже существующий объект
|
||||
if (!e.getStatus().getCode().equals(HttpStatus.CONFLICT.value())) {
|
||||
throw e;
|
||||
}
|
||||
log.warn("Tried to create already existing rule", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<PrometheusRule> getPrometheusRule(String ruleName) throws KubernetesClientException {
|
||||
try (KubernetesClient client = new KubernetesClientBuilder().withConfig(k8sConfig).build()) {
|
||||
MixedOperation<PrometheusRule, KubernetesResourceList<PrometheusRule>, Resource<PrometheusRule>>
|
||||
prometheusRuleClient = client.resources(PrometheusRule.class);
|
||||
var rule = prometheusRuleClient.withName(ruleName).get();
|
||||
return Optional.ofNullable(rule);
|
||||
}
|
||||
}
|
||||
|
||||
public Set<PrometheusRuleSpec.Rule> getPrometheusRuleGroupAlerts(String ruleName,
|
||||
String groupName)
|
||||
throws KubernetesClientException {
|
||||
try (KubernetesClient client = new KubernetesClientBuilder().withConfig(k8sConfig).build()) {
|
||||
MixedOperation<PrometheusRule, KubernetesResourceList<PrometheusRule>, Resource<PrometheusRule>>
|
||||
prometheusRuleClient = client.resources(PrometheusRule.class);
|
||||
var rule =
|
||||
prometheusRuleClient.inNamespace(client.getNamespace()).withName(ruleName).get();
|
||||
if (rule == null) {
|
||||
return Set.of();
|
||||
}
|
||||
var groupAlerts = rule.getSpec().getGroups().stream()
|
||||
.filter(group -> group.getName().equals(groupName))
|
||||
.findFirst().orElse(new PrometheusRuleSpec.Group()).getRules();
|
||||
return groupAlerts == null ? Set.of() : groupAlerts;
|
||||
}
|
||||
}
|
||||
|
||||
public void deletePrometheusRuleGroup(String ruleName, String groupName) throws KubernetesClientException {
|
||||
modifyPrometheusRule(ruleName, PrometheusFunctionsUtil.getRemoveGroupByNameFunc(groupName));
|
||||
}
|
||||
|
||||
public void deleteAlertFromPrometheusRuleGroup(String ruleName, String groupName, String alertNameForRemoval)
|
||||
throws KubernetesClientException {
|
||||
modifyPrometheusRule(ruleName, PrometheusFunctionsUtil.getRemoveAlertByGroupAndNameFunc(groupName,
|
||||
alertNameForRemoval));
|
||||
}
|
||||
|
||||
public void addAlertToPrometheusRuleGroup(String ruleName, String groupName, PrometheusRuleSpec.Rule alert)
|
||||
throws KubernetesClientException {
|
||||
modifyPrometheusRule(ruleName, PrometheusFunctionsUtil.getAddAlertToGroupFunc(groupName, alert));
|
||||
}
|
||||
|
||||
private void modifyPrometheusRule(String ruleName,
|
||||
UnaryOperator<PrometheusRule> modifyFunc) throws KubernetesClientException {
|
||||
try (KubernetesClient client = new KubernetesClientBuilder().withConfig(k8sConfig).build()) {
|
||||
MixedOperation<PrometheusRule, KubernetesResourceList<PrometheusRule>, Resource<PrometheusRule>>
|
||||
prometheusRuleClient = client.resources(PrometheusRule.class);
|
||||
var rule =
|
||||
prometheusRuleClient.inNamespace(client.getNamespace()).withName(ruleName).get();
|
||||
var resource = prometheusRuleClient.inNamespace(client.getNamespace()).resource(rule);
|
||||
var response = resource.edit(modifyFunc);
|
||||
log.info("Rule after modification: {}", response);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package dev.vality.alerting.mayday.prometheus.client.k8s.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import io.fabric8.kubernetes.api.model.Namespaced;
|
||||
import io.fabric8.kubernetes.client.CustomResource;
|
||||
import io.fabric8.kubernetes.model.annotation.Group;
|
||||
import io.fabric8.kubernetes.model.annotation.Kind;
|
||||
import io.fabric8.kubernetes.model.annotation.Version;
|
||||
|
||||
|
||||
@Version("v1")
|
||||
@Group("monitoring.coreos.com")
|
||||
@Kind("PrometheusRule")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class PrometheusRule extends CustomResource<PrometheusRuleSpec, PrometheusRuleStatus> implements Namespaced {
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
package dev.vality.alerting.mayday.prometheus.client.k8s.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@Data
|
||||
public class PrometheusRuleSpec {
|
||||
|
||||
private Set<Group> groups = new HashSet<>();
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
public static class Group {
|
||||
private String name;
|
||||
private String duration;
|
||||
private Set<Rule> rules;
|
||||
@JsonProperty("partial_response_strategy")
|
||||
private String partialResponseStrategy;
|
||||
private Integer limit;
|
||||
}
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
public static class Rule {
|
||||
private String record;
|
||||
private String alert;
|
||||
private String expr;
|
||||
@JsonProperty("for")
|
||||
private String duration;
|
||||
@JsonProperty("keep_firing_for")
|
||||
private String keepFiringFor;
|
||||
private Map<String, String> labels;
|
||||
private Map<String, String> annotations;
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package dev.vality.alerting.mayday.prometheus.client.k8s.model;
|
||||
|
||||
|
||||
public class PrometheusRuleStatus {
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
package dev.vality.alerting.mayday.prometheus.client.k8s.util;
|
||||
|
||||
import dev.vality.alerting.mayday.prometheus.client.k8s.model.PrometheusRule;
|
||||
import dev.vality.alerting.mayday.prometheus.client.k8s.model.PrometheusRuleSpec;
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
@UtilityClass
|
||||
public class PrometheusFunctionsUtil {
|
||||
|
||||
public static UnaryOperator<PrometheusRule> getRemoveGroupByNameFunc(String groupName) {
|
||||
return prometheusRule -> {
|
||||
var groups = prometheusRule.getSpec().getGroups();
|
||||
var groupIterator = groups.iterator();
|
||||
while (groupIterator.hasNext()) {
|
||||
var group = groupIterator.next();
|
||||
if (group.getName().equals(groupName)) {
|
||||
groupIterator.remove();
|
||||
break;
|
||||
}
|
||||
}
|
||||
return prometheusRule;
|
||||
};
|
||||
}
|
||||
|
||||
public static UnaryOperator<PrometheusRule> getRemoveAlertByGroupAndNameFunc(String groupName,
|
||||
String alertNameForRemoval) {
|
||||
return prometheusRule -> {
|
||||
var groups = prometheusRule.getSpec().getGroups();
|
||||
var groupIterator = groups.iterator();
|
||||
while (groupIterator.hasNext()) {
|
||||
var group = groupIterator.next();
|
||||
if (group.getName().equals(groupName)) {
|
||||
Set<PrometheusRuleSpec.Rule> alertRules = group.getRules();
|
||||
var ruleIterator = alertRules.iterator();
|
||||
while (ruleIterator.hasNext()) {
|
||||
var rule = ruleIterator.next();
|
||||
if (rule.getAlert().equals(alertNameForRemoval)) {
|
||||
ruleIterator.remove();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (group.getRules().isEmpty()) {
|
||||
groupIterator.remove();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return prometheusRule;
|
||||
};
|
||||
}
|
||||
|
||||
public static UnaryOperator<PrometheusRule> getAddAlertToGroupFunc(String groupName,
|
||||
PrometheusRuleSpec.Rule alert) {
|
||||
return prometheusRule -> {
|
||||
var groups = prometheusRule.getSpec().getGroups();
|
||||
if (groups == null) {
|
||||
groups = new HashSet<>();
|
||||
prometheusRule.getSpec().setGroups(groups);
|
||||
}
|
||||
PrometheusRuleSpec.Group group = groups.stream().filter(g -> g.getName().equals(groupName)).findFirst()
|
||||
.orElse(createPrometheusRuleGroup(groupName));
|
||||
groups.add(group);
|
||||
var rules = group.getRules();
|
||||
if (rules == null) {
|
||||
rules = new HashSet<>();
|
||||
group.setRules(rules);
|
||||
}
|
||||
rules.add(alert);
|
||||
return prometheusRule;
|
||||
};
|
||||
}
|
||||
|
||||
public static PrometheusRuleSpec.Group createPrometheusRuleGroup(String groupName) {
|
||||
var group = new PrometheusRuleSpec.Group();
|
||||
group.setName(groupName);
|
||||
group.setRules(new HashSet<>());
|
||||
return group;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package dev.vality.alerting.mayday.prometheus.config.properties;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "k8s.prometheus-rule")
|
||||
@Getter
|
||||
@Setter
|
||||
public class K8sPrometheusRuleProperties {
|
||||
private Map<String, String> labels;
|
||||
private AlertRule alertRule;
|
||||
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public static class AlertRule {
|
||||
private Map<String, String> labels;
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
package dev.vality.alerting.mayday.prometheus.converter;
|
||||
|
||||
import dev.vality.alerting.mayday.prometheus.client.k8s.model.PrometheusRuleSpec;
|
||||
import dev.vality.alerting.mayday.common.constant.PrometheusRuleAnnotation;
|
||||
import dev.vality.alerting.mayday.common.constant.PrometheusRuleLabel;
|
||||
import dev.vality.alerting.mayday.common.dto.CreateAlertDto;
|
||||
import dev.vality.alerting.mayday.prometheus.config.properties.K8sPrometheusRuleProperties;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class CreateAlertDtoToPrometheusRuleConverter implements Converter<CreateAlertDto, PrometheusRuleSpec.Rule> {
|
||||
|
||||
private final K8sPrometheusRuleProperties k8sPrometheusRuleProperties;
|
||||
|
||||
@Value("${spring.application.name}")
|
||||
private String applicationName;
|
||||
|
||||
@Override
|
||||
public PrometheusRuleSpec.Rule convert(CreateAlertDto source) {
|
||||
PrometheusRuleSpec.Rule rule = new PrometheusRuleSpec.Rule();
|
||||
rule.setAlert(source.getAlertId());
|
||||
rule.setExpr(source.getPrometheusQuery());
|
||||
rule.setAnnotations(Map.of(PrometheusRuleAnnotation.ALERT_NAME, source.getUserFriendlyAlertName(),
|
||||
PrometheusRuleAnnotation.ALERT_DESCRIPTION, source.getUserFriendlyAlertDescription()));
|
||||
Map<String, String> labels = new HashMap<>(k8sPrometheusRuleProperties.getAlertRule().getLabels());
|
||||
labels.put(PrometheusRuleLabel.USERNAME, source.getUserId());
|
||||
labels.put(PrometheusRuleLabel.SERVICE, applicationName);
|
||||
rule.setLabels(labels);
|
||||
return rule;
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package dev.vality.alerting.mayday.prometheus.converter;
|
||||
|
||||
import dev.vality.alerting.mayday.UserAlert;
|
||||
import dev.vality.alerting.mayday.prometheus.client.k8s.model.PrometheusRuleSpec;
|
||||
import dev.vality.alerting.mayday.common.constant.PrometheusRuleAnnotation;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class PrometheusRuleToUserAlertConverter implements Converter<PrometheusRuleSpec.Rule, UserAlert> {
|
||||
@Override
|
||||
public UserAlert convert(PrometheusRuleSpec.Rule source) {
|
||||
return new UserAlert()
|
||||
.setId(source.getAlert())
|
||||
.setName(source.getAnnotations().get(PrometheusRuleAnnotation.ALERT_NAME));
|
||||
}
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
package dev.vality.alerting.mayday.prometheus.service;
|
||||
|
||||
import dev.vality.alerting.mayday.UserAlert;
|
||||
import dev.vality.alerting.mayday.common.dto.CreateAlertDto;
|
||||
import dev.vality.alerting.mayday.prometheus.client.k8s.PrometheusClient;
|
||||
import dev.vality.alerting.mayday.prometheus.client.k8s.model.PrometheusRule;
|
||||
import dev.vality.alerting.mayday.prometheus.client.k8s.model.PrometheusRuleSpec;
|
||||
import dev.vality.alerting.mayday.prometheus.config.properties.K8sPrometheusRuleProperties;
|
||||
import io.fabric8.kubernetes.api.model.ObjectMeta;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class PrometheusService {
|
||||
|
||||
@Value("${spring.application.name}")
|
||||
private String applicationName;
|
||||
private String prometheusRuleName;
|
||||
|
||||
private final K8sPrometheusRuleProperties k8SPrometheusRuleProperties;
|
||||
private final PrometheusClient prometheusClient;
|
||||
private final Converter<CreateAlertDto, PrometheusRuleSpec.Rule> createAlertDtoToPrometheusRuleConverter;
|
||||
private final Converter<PrometheusRuleSpec.Rule, UserAlert> prometheusRuleToUserAlertConverter;
|
||||
|
||||
public void deleteAllUserAlerts(String userId) {
|
||||
if (prometheusClient.getPrometheusRule(getPrometheusRuleName()).isEmpty()) {
|
||||
return;
|
||||
}
|
||||
prometheusClient.deletePrometheusRuleGroup(getPrometheusRuleName(), userId);
|
||||
}
|
||||
|
||||
public void deleteUserAlert(String userId, String alertId) {
|
||||
if (prometheusClient.getPrometheusRule(getPrometheusRuleName()).isEmpty()) {
|
||||
return;
|
||||
}
|
||||
prometheusClient.deleteAlertFromPrometheusRuleGroup(getPrometheusRuleName(), userId, alertId);
|
||||
}
|
||||
|
||||
public List<UserAlert> getUserAlerts(String userId) {
|
||||
if (prometheusClient.getPrometheusRule(getPrometheusRuleName()).isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
return prometheusClient.getPrometheusRuleGroupAlerts(getPrometheusRuleName(), userId).stream()
|
||||
.map(prometheusRuleToUserAlertConverter::convert)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public void createUserAlert(CreateAlertDto createAlertDto) {
|
||||
if (prometheusClient.getPrometheusRule(getPrometheusRuleName()).isEmpty()) {
|
||||
log.info("Prometheus rule '{}' not found and will be created", getPrometheusRuleName());
|
||||
prometheusClient.createPrometheusRule(buildPrometheusRule());
|
||||
}
|
||||
PrometheusRuleSpec.Rule alertRule = createAlertDtoToPrometheusRuleConverter.convert(createAlertDto);
|
||||
log.info("New alert configuration: {}", alertRule);
|
||||
prometheusClient.addAlertToPrometheusRuleGroup(getPrometheusRuleName(), createAlertDto.getUserId(), alertRule);
|
||||
}
|
||||
|
||||
public String getPrometheusRuleName() {
|
||||
if (ObjectUtils.isEmpty(prometheusRuleName)) {
|
||||
prometheusRuleName = "%s-managed-rule".formatted(applicationName);
|
||||
}
|
||||
return prometheusRuleName;
|
||||
}
|
||||
|
||||
private PrometheusRule buildPrometheusRule() {
|
||||
PrometheusRule rule = new PrometheusRule();
|
||||
var metadata = new ObjectMeta();
|
||||
metadata.setLabels(k8SPrometheusRuleProperties.getLabels());
|
||||
metadata.setName(getPrometheusRuleName());
|
||||
rule.setMetadata(metadata);
|
||||
PrometheusRuleSpec spec = new PrometheusRuleSpec();
|
||||
rule.setSpec(spec);
|
||||
return rule;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
package dev.vality.alerting.mayday.thrift.converter;
|
||||
|
||||
import dev.vality.alerting.mayday.AlertConfiguration;
|
||||
import dev.vality.alerting.mayday.ParameterConfiguration;
|
||||
import dev.vality.alerting.mayday.common.constant.AlertConfigurationRequiredParameter;
|
||||
import dev.vality.alerting.mayday.alerttemplate.model.alerttemplate.AlertTemplate;
|
||||
import dev.vality.alerting.mayday.alerttemplate.service.DictionaryService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class AlertParamsToAlertConfiguration implements Converter<List<AlertTemplate.AlertConfigurationParameter>,
|
||||
AlertConfiguration> {
|
||||
|
||||
private final DictionaryService dictionaryService;
|
||||
|
||||
@Override
|
||||
public AlertConfiguration convert(List<AlertTemplate.AlertConfigurationParameter> alertParams) {
|
||||
var alertConfiguration = new AlertConfiguration();
|
||||
alertConfiguration.setParameters(alertParams.stream().map(param -> new ParameterConfiguration()
|
||||
.setId(param.getId().toString())
|
||||
.setName(param.getReadableName())
|
||||
.setMandatory(param.getMandatory())
|
||||
.setValueRegexp(param.getRegexp())
|
||||
.setOptions(param.getDictionaryName() != null ? dictionaryService
|
||||
.getDictionary(param.getDictionaryName()).keySet().stream().toList()
|
||||
: null)
|
||||
.setMultipleValues(param.getMultipleValues()))
|
||||
.collect(Collectors.toList()));
|
||||
alertConfiguration.getParameters().addAll(Arrays.stream(AlertConfigurationRequiredParameter.values())
|
||||
.map(requiredParameter ->
|
||||
new ParameterConfiguration()
|
||||
.setId(String.valueOf(requiredParameter.getId()))
|
||||
.setName(requiredParameter.getReadableName())
|
||||
.setMandatory(true)
|
||||
.setValueRegexp(requiredParameter.getRegexp())
|
||||
).toList());
|
||||
return alertConfiguration;
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package dev.vality.alerting.mayday.thrift.converter;
|
||||
|
||||
import dev.vality.alerting.mayday.Alert;
|
||||
import dev.vality.alerting.mayday.alerttemplate.model.alerttemplate.AlertTemplate;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class AlertTemplateToAlertConverter implements Converter<AlertTemplate, Alert> {
|
||||
@Override
|
||||
public Alert convert(AlertTemplate source) {
|
||||
return new Alert()
|
||||
.setId(source.getId())
|
||||
.setName(source.getReadableName());
|
||||
}
|
||||
}
|
@ -0,0 +1,97 @@
|
||||
package dev.vality.alerting.mayday.thrift.service;
|
||||
|
||||
import dev.vality.alerting.mayday.*;
|
||||
import dev.vality.alerting.mayday.alertmanager.service.AlertmanagerService;
|
||||
import dev.vality.alerting.mayday.common.dto.CreateAlertDto;
|
||||
import dev.vality.alerting.mayday.alerttemplate.model.alerttemplate.AlertTemplate;
|
||||
import dev.vality.alerting.mayday.prometheus.service.PrometheusService;
|
||||
import dev.vality.alerting.mayday.alerttemplate.service.helper.TemplateHelper;
|
||||
import dev.vality.alerting.mayday.alerttemplate.service.TemplateService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AlertingService implements AlertingServiceSrv.Iface {
|
||||
|
||||
private final TemplateService templateService;
|
||||
private final PrometheusService prometheusService;
|
||||
private final AlertmanagerService alertmanagerService;
|
||||
|
||||
private final TemplateHelper templateHelper;
|
||||
|
||||
private final Converter<AlertTemplate, Alert> alertTemplateAlertConverter;
|
||||
private final Converter<List<AlertTemplate.AlertConfigurationParameter>, AlertConfiguration>
|
||||
alertParamsToAlertConfiguration;
|
||||
|
||||
@Override
|
||||
public void deleteAllAlerts(String userId) {
|
||||
log.info("Removing all alerts for user '{}'", userId);
|
||||
alertmanagerService.deleteUserRoutes(userId);
|
||||
prometheusService.deleteAllUserAlerts(userId);
|
||||
log.info("Removed all alerts for user '{}'", userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteAlert(String userId, String alertId) {
|
||||
log.info("Removing alert '{}' for user '{}'", alertId, userId);
|
||||
alertmanagerService.deleteUserRoute(alertId);
|
||||
prometheusService.deleteUserAlert(userId, alertId);
|
||||
log.info("Removed alert '{}' for user '{}'", alertId, userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<UserAlert> getUserAlerts(String userId) {
|
||||
log.info("Retrieving all alerts for user '{}'", userId);
|
||||
List<UserAlert> userAlerts = prometheusService.getUserAlerts(userId);
|
||||
log.info("Retrieved {} alerts for user '{}'", userAlerts.size(), userId);
|
||||
return userAlerts;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Alert> getSupportedAlerts() {
|
||||
log.info("Retrieving all supported alerts");
|
||||
List<AlertTemplate> metricTemplates =
|
||||
templateService.getAlertTemplates();
|
||||
List<Alert> supportedAlerts =
|
||||
metricTemplates.stream()
|
||||
.map(alertTemplateAlertConverter::convert)
|
||||
.sorted(Comparator.comparing(a -> a.getName()))
|
||||
.collect(Collectors.toList());
|
||||
log.info("Retrieved {} supported alerts", supportedAlerts.size());
|
||||
return supportedAlerts;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AlertConfiguration getAlertConfiguration(String alertTemplateId) {
|
||||
log.info("Retrieving configuration for alert '{}'", alertTemplateId);
|
||||
List<AlertTemplate.AlertConfigurationParameter> metricParams =
|
||||
templateService.getAlertTemplateParams(alertTemplateId);
|
||||
AlertConfiguration alertConfiguration = alertParamsToAlertConfiguration.convert(metricParams);
|
||||
alertConfiguration.setId(alertTemplateId);
|
||||
log.info("Successfully retrieved configuration for alert '{}': {}", alertTemplateId, alertConfiguration);
|
||||
return alertConfiguration;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createAlert(CreateAlertRequest createAlertRequest) {
|
||||
log.info("Processing CreateAlertRequest: '{}'", createAlertRequest);
|
||||
List<AlertTemplate.AlertConfigurationParameter> metricParams =
|
||||
templateService.getAlertTemplateParams(createAlertRequest.getAlertId());
|
||||
AlertTemplate metricTemplate =
|
||||
templateService.getAlertTemplateById(createAlertRequest.getAlertId());
|
||||
CreateAlertDto createAlertDto =
|
||||
templateHelper.preparePrometheusRuleData(createAlertRequest, metricTemplate, metricParams);
|
||||
prometheusService.createUserAlert(createAlertDto);
|
||||
alertmanagerService.createUserRoute(createAlertDto);
|
||||
log.info("CreateAlertRequest processed successfully: '{}'", createAlertRequest);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package dev.vality.alerting.mayday.thrift.servlet;
|
||||
|
||||
import dev.vality.alerting.mayday.AlertingServiceSrv;
|
||||
import dev.vality.woody.thrift.impl.http.THServiceBuilder;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import javax.servlet.*;
|
||||
import javax.servlet.annotation.WebServlet;
|
||||
import java.io.IOException;
|
||||
|
||||
@WebServlet("/mayday")
|
||||
@RequiredArgsConstructor
|
||||
public class AlertingServlet extends GenericServlet {
|
||||
|
||||
private Servlet thriftServlet;
|
||||
|
||||
@Autowired
|
||||
private AlertingServiceSrv.Iface requestHandler;
|
||||
|
||||
@Override
|
||||
public void init(ServletConfig config) throws ServletException {
|
||||
super.init(config);
|
||||
thriftServlet = new THServiceBuilder()
|
||||
.build(AlertingServiceSrv.Iface.class, requestHandler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
|
||||
thriftServlet.service(req, res);
|
||||
}
|
||||
}
|
60
src/main/resources/application.yml
Normal file
60
src/main/resources/application.yml
Normal file
@ -0,0 +1,60 @@
|
||||
server:
|
||||
port: '8022'
|
||||
|
||||
management:
|
||||
server:
|
||||
port: '${management.port}'
|
||||
metrics:
|
||||
export:
|
||||
prometheus:
|
||||
enabled: true
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
metrics:
|
||||
enabled: true
|
||||
prometheus:
|
||||
enabled: true
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,prometheus
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: 'mayday'
|
||||
output:
|
||||
ansi:
|
||||
enabled: always
|
||||
datasource:
|
||||
type: com.zaxxer.hikari.HikariDataSource
|
||||
driver-class-name: org.postgresql.Driver
|
||||
url: jdbc:postgresql://localhost:5432/daway
|
||||
username: postgres
|
||||
password: postgres
|
||||
|
||||
k8s:
|
||||
namespace:
|
||||
prometheus-rule:
|
||||
labels:
|
||||
release: prometheus
|
||||
alert-rule:
|
||||
labels:
|
||||
# Лейбл с неймспейсом необходим, поскольку алертменеджер по умолчанию начинает фильтровать по нему.
|
||||
# Тут описано более подробно: https://github.com/prometheus-operator/prometheus-operator/discussions/3733
|
||||
namespace: default
|
||||
alertmanager-configuration:
|
||||
labels:
|
||||
alertmanager: alertmanager
|
||||
alertmanager:
|
||||
webhook:
|
||||
url: http://localhost:8022
|
||||
path: /alertmanager/webhook
|
||||
receiver:
|
||||
telegram-bot:
|
||||
url: http://telegram:1234/endp
|
||||
|
||||
|
||||
testcontainers:
|
||||
postgresql:
|
||||
tag: '13'
|
48
src/main/resources/template/limit_payment.json
Normal file
48
src/main/resources/template/limit_payment.json
Normal file
@ -0,0 +1,48 @@
|
||||
{
|
||||
"id": "limit_payment",
|
||||
"readable_name": "Лимит для платежей",
|
||||
"prometheus_query": "round(100 * (el_payment_limits_amount_by_calendar{provider_id=~\"${provider_id}\", terminal_id=~\"${terminal_id}\", shop_id=~\"${shop_id}\", limit_scope_types=~\"${limit_scope}\"}/el_payment_limits_boundary_by_calendar{provider_id=~\"${provider_id}\", terminal_id=~\"${terminal_id}\", shop_id=~\"${shop_id}\", limit_scope_types=~\"${limit_scope}\"}), 1) > ${limit_percentage_threshold}",
|
||||
"alert_name_template": "Лимит платежей для терминала '${terminal_id}', магазина '${shop_id}', провайдера '${provider_id}' c глубиной '${limit_scope}' израсходован на ${limit_percentage_threshold}%",
|
||||
"alert_notification_template": "Лимит платежей для терминала '${terminal_id}', магазина '${shop_id}', провайдера '${provider_id}' c глубиной '${limit_scope}' израсходован на {{ $value }}%",
|
||||
"parameters": [
|
||||
{
|
||||
"id": 1,
|
||||
"substitution_name": "limit_scope",
|
||||
"readable_name": "Введите глубину лимита",
|
||||
"mandatory": true,
|
||||
"multiple_values": false,
|
||||
"dictionary_name": "payment_limit_scopes"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"substitution_name": "provider_id",
|
||||
"readable_name": "Введите идентификатор провайдера",
|
||||
"mandatory": false,
|
||||
"multiple_values": false,
|
||||
"dictionary_name": "payment_providers"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"substitution_name": "terminal_id",
|
||||
"readable_name": "Введите идентификатор терминала",
|
||||
"mandatory": false,
|
||||
"multiple_values": false,
|
||||
"dictionary_name": "payment_terminals"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"substitution_name": "shop_id",
|
||||
"readable_name": "Введите идентификатор магазина",
|
||||
"mandatory": false,
|
||||
"multiple_values": false,
|
||||
"dictionary_name": "shops"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"substitution_name": "limit_percentage_threshold",
|
||||
"readable_name": "Введите степень израсходования лимита в процентах (Пример: 85), при достижении которой вас необходимо уведомить",
|
||||
"mandatory": true,
|
||||
"regexp": "^[0-9][0-9]?$|^100$"
|
||||
}
|
||||
]
|
||||
}
|
48
src/main/resources/template/limit_withdrawal.json
Normal file
48
src/main/resources/template/limit_withdrawal.json
Normal file
@ -0,0 +1,48 @@
|
||||
{
|
||||
"id": "limit_withdrawal",
|
||||
"readable_name": "Лимит для выплат",
|
||||
"prometheus_query": "round(100 * (el_payout_limits_amount_by_calendar{provider_id=~\"${provider_id}\", terminal_id=~\"${terminal_id}\", wallet_id=~\"${wallet_id}\", limit_scope_types=\"${limit_scope}\"}/el_payout_limits_boundary_by_calendar{provider_id=~\"${provider_id}\", terminal_id=~\"${terminal_id}\", wallet_id=~\"${wallet_id}\", limit_scope_types=\"${limit_scope}\"}), 1) > ${limit_percentage_threshold}",
|
||||
"alert_name_template": "Месячный лимит выплат для терминала '${terminal_id}', кошелька '${wallet_id}', провайдера '${provider_id}' c глубиной '${limit_scope}' израсходован на ${limit_percentage_threshold}%",
|
||||
"alert_notification_template": "Месячный лимит выплат для терминала '${terminal_id}', кошелька '${wallet_id}', провайдера '${provider_id}' c глубиной '${limit_scope}' израсходован на {{ $value }}%",
|
||||
"parameters": [
|
||||
{
|
||||
"id": 1,
|
||||
"substitution_name": "limit_scope",
|
||||
"readable_name": "Введите глубину лимита",
|
||||
"mandatory": true,
|
||||
"multiple_values": false,
|
||||
"dictionary_name": "payout_limit_scopes"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"substitution_name": "provider_id",
|
||||
"readable_name": "Введите идентификатор провайдера",
|
||||
"mandatory": false,
|
||||
"multiple_values": false,
|
||||
"dictionary_name": "payout_providers"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"substitution_name": "terminal_id",
|
||||
"readable_name": "Введите идентификатор терминала",
|
||||
"mandatory": false,
|
||||
"multiple_values": false,
|
||||
"dictionary_name": "payout_terminals"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"substitution_name": "wallet_id",
|
||||
"readable_name": "Введите идентификатор кошелька",
|
||||
"mandatory": false,
|
||||
"multiple_values": false,
|
||||
"dictionary_name": "wallets"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"substitution_name": "limit_percentage_threshold",
|
||||
"readable_name": "Введите степень израсходования лимита в процентах (Пример: 85), при достижении которой вас необходимо уведомить",
|
||||
"mandatory": true,
|
||||
"regexp": "^[0-9][0-9]?$|^100$"
|
||||
}
|
||||
]
|
||||
}
|
62
src/main/resources/template/payment_conversion.json
Normal file
62
src/main/resources/template/payment_conversion.json
Normal file
@ -0,0 +1,62 @@
|
||||
{
|
||||
"id": "payment_conversion",
|
||||
"readable_name": "Конверсия платежей",
|
||||
"prometheus_query": "round(100 * sum(ebm_payments_status_count{provider_id=~\"${provider_id}\", terminal_id=~\"${terminal_id}\",shop_id=~\"${shop_id}\",currency=~\"${currency}\",duration=\"${conversion_period}\",status=\"captured\"}) / sum(ebm_payments_status_count{provider_id=~\"${provider_id}\",terminal_id=~\"${terminal_id}\",shop_id=~\"${shop_id}\",currency=~\"${currency}\",duration=\"${conversion_period}\"}), 1) ${boundary_type} ${conversion_rate_threshold}",
|
||||
"alert_name_template": "Конверсия платежей по провайдеру '${provider_id}', терминалу '${terminal_id}', валюте '${currency}' и магазину '${shop_id}' за период: ${conversion_period} ${boundary_type} ${conversion_rate_threshold}%",
|
||||
"alert_notification_template": "Конверсия платежей по провайдеру '${provider_id}', терминалу '${terminal_id}', валюте '${currency}' и магазину '${shop_id}' за период: ${conversion_period} ${boundary_type} ${conversion_rate_threshold}%! Текущее значение: {{ $value }}%",
|
||||
"parameters": [
|
||||
{
|
||||
"id": 1,
|
||||
"substitution_name": "provider_id",
|
||||
"readable_name": "Введите идентификатор провайдера (опционально)",
|
||||
"mandatory": false,
|
||||
"multiple_values": true,
|
||||
"dictionary_name": "payment_providers"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"substitution_name": "terminal_id",
|
||||
"readable_name": "Введите идентификатор терминала (опционально)",
|
||||
"mandatory": false,
|
||||
"multiple_values": true,
|
||||
"dictionary_name": "payment_terminals"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"substitution_name": "shop_id",
|
||||
"readable_name": "Введите идентификатор магазина (опционально)",
|
||||
"mandatory": false,
|
||||
"multiple_values": true,
|
||||
"dictionary_name": "shops"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"substitution_name": "currency",
|
||||
"readable_name": "Введите код валюты (опционально)",
|
||||
"mandatory": false,
|
||||
"multiple_values": false,
|
||||
"dictionary_name": "currencies"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"substitution_name": "boundary_type",
|
||||
"readable_name": "Выберите тип условия для алерта",
|
||||
"mandatory": true,
|
||||
"dictionary_name": "boundaries"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"substitution_name": "conversion_rate_threshold",
|
||||
"readable_name": "Введите порог конверсии в процентах (Пример: 85)",
|
||||
"mandatory": true,
|
||||
"regexp": "^[0-9][0-9]?$|^100$"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"substitution_name": "conversion_period",
|
||||
"readable_name": "Период в минутах, за который необходимо проверять конверсию",
|
||||
"mandatory": true,
|
||||
"dictionary_name": "aggregation_intervals"
|
||||
}
|
||||
]
|
||||
}
|
75
src/main/resources/template/payment_limit_custom.json
Normal file
75
src/main/resources/template/payment_limit_custom.json
Normal file
@ -0,0 +1,75 @@
|
||||
{
|
||||
"id": "payment_limit_custom_hours",
|
||||
"readable_name": "Оборот (эквайринг) в определенные часы",
|
||||
"prometheus_query": "(sum(ebm_payments_amount{provider_id=~\"${provider_id}\",shop_id=~\"${shop_id}\",status=\"captured\",terminal_id=~\"${terminal_id}\",duration=\"${aggr_period}\"}) OR on() vector(0)) ${boundary_type} ${amount_threshold} ${time_interval_boundary} hour() >= ${time_interval_start_hour} < ${time_interval_end_hour}",
|
||||
"alert_name_template": "Оборот платежей по провайдеру '${provider_id}', терминалу '${terminal_id}' и магазину '${shop_id}' за период: ${aggr_period} ${boundary_type} ${amount_threshold}. Временной интервал с ${time_interval_start_hour} UTC по ${time_interval_end_hour} UTC",
|
||||
"alert_notification_template": "Оборот платежей по провайдеру '${provider_id}', терминалу '${terminal_id}' и магазину '${shop_id}' в интервале с ${time_interval_start_hour} UTC по ${time_interval_end_hour} UTC за период: ${aggr_period} ${boundary_type} ${amount_threshold}! Текущее значение: {{ $value }}",
|
||||
"parameters": [
|
||||
{
|
||||
"id": 1,
|
||||
"substitution_name": "provider_id",
|
||||
"readable_name": "Введите идентификатор провайдера (опционально)",
|
||||
"mandatory": false,
|
||||
"multiple_values": true,
|
||||
"dictionary_name": "payment_providers"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"substitution_name": "terminal_id",
|
||||
"readable_name": "Введите идентификатор терминала (опционально)",
|
||||
"mandatory": false,
|
||||
"multiple_values": true,
|
||||
"dictionary_name": "payment_terminals"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"substitution_name": "shop_id",
|
||||
"readable_name": "Введите идентификатор магазина (опционально)",
|
||||
"mandatory": false,
|
||||
"multiple_values": true,
|
||||
"dictionary_name": "shops"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"substitution_name": "aggr_period",
|
||||
"readable_name": "Период, за который необходимо считать сумму по всем успешным платежам",
|
||||
"mandatory": true,
|
||||
"dictionary_name": "aggregation_intervals"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"substitution_name": "amount_threshold",
|
||||
"readable_name": "Введите пороговое значение суммы",
|
||||
"mandatory": true,
|
||||
"regexp": "^\\d+$"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"substitution_name": "boundary_type",
|
||||
"readable_name": "Выберите тип условия для алерта",
|
||||
"mandatory": true,
|
||||
"dictionary_name": "boundaries"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"substitution_name": "time_interval_start_hour",
|
||||
"readable_name": "Введите начало временного интервала в часовом поясе UTC (часы в формате 0-23), когда необходимо проверять сумму. Если вы хотите проверять сумму платежей на протяжении всего дня, введите 0.",
|
||||
"mandatory": true,
|
||||
"regexp": "^2[0-3]|1[0-9]|[0-9]"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"substitution_name": "time_interval_end_hour",
|
||||
"readable_name": "Введите конец временного интервала в часовом поясе UTC (часы в формате 0-23), когда необходимо проверять сумму. Если вы хотите проверять сумму платежей на протяжении всего дня, введите 0.",
|
||||
"mandatory": true,
|
||||
"regexp": "^2[0-3]|1[0-9]|[0-9]"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"substitution_name": "time_interval_boundary",
|
||||
"readable_name": "Временной интервал, указанный вами, захватывает несколько календарных дней/целый календарный день?",
|
||||
"mandatory": true,
|
||||
"dictionary_name": "time_interval_boundaries"
|
||||
}
|
||||
]
|
||||
}
|
55
src/main/resources/template/payment_transactions.json
Normal file
55
src/main/resources/template/payment_transactions.json
Normal file
@ -0,0 +1,55 @@
|
||||
{
|
||||
"id": "payment_transactions",
|
||||
"readable_name": "Количество платежных транзакций",
|
||||
"prometheus_query": "(sum(ebm_payments_status_count{terminal_id=~\"${terminal_id}\",provider_id=~\"${provider_id}\",shop_id=~\"${shop_id}\",duration=\"${aggr_period}\"}) OR on() vector(0)) ${boundary_type} ${transaction_num_threshold}",
|
||||
"alert_name_template": "Количество платежных транзакций у провайдера ${provider_id}, терминала ${terminal_id} и магазина ${shop_id} за период: ${aggr_period} ${boundary_type} ${transaction_num_threshold}",
|
||||
"alert_notification_template": "Количество платежных транзакций у провайдера ${provider_id}, терминала ${terminal_id} и магазина ${shop_id} за период: ${aggr_period} ${boundary_type} ${transaction_num_threshold}! Текущее значение: {{ $value }}",
|
||||
"parameters": [
|
||||
{
|
||||
"id": 1,
|
||||
"substitution_name": "provider_id",
|
||||
"readable_name": "Введите идентификатор провайдера (опционально)",
|
||||
"mandatory": false,
|
||||
"multiple_values": true,
|
||||
"dictionary_name": "payment_providers"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"substitution_name": "terminal_id",
|
||||
"readable_name": "Введите идентификатор терминала (опционально)",
|
||||
"mandatory": false,
|
||||
"multiple_values": true,
|
||||
"dictionary_name": "payment_terminals"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"substitution_name": "shop_id",
|
||||
"readable_name": "Введите идентификатор магазина (опционально)",
|
||||
"mandatory": false,
|
||||
"multiple_values": true,
|
||||
"dictionary_name": "shops"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"substitution_name": "boundary_type",
|
||||
"readable_name": "Выберите тип условия для алерта",
|
||||
"mandatory": true,
|
||||
"dictionary_name": "boundaries"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"substitution_name": "transaction_num_threshold",
|
||||
"readable_name": "Введите пороговое количество транзакций (Пример: 10000)",
|
||||
"mandatory": true,
|
||||
"regexp": "^\\d+$"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"substitution_name": "aggr_period",
|
||||
"readable_name": "Период, за который необходимо проверять количество транзакций (Пример: 60)",
|
||||
"mandatory": true,
|
||||
"dictionary_name": "aggregation_intervals"
|
||||
}
|
||||
]
|
||||
|
||||
}
|
25
src/main/resources/template/wallet_balance.json
Normal file
25
src/main/resources/template/wallet_balance.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"id": "wallet_balance",
|
||||
"readable_name": "Остаток на балансе кошелька",
|
||||
"prometheus_query": "ewb_wallet_balances_amount{wallet_id=\"${wallet_id}\"} < ${wallet_amount_threshold}",
|
||||
"alert_name_template": "Баланс кошелька '${wallet_id}' < ${wallet_amount_threshold}!",
|
||||
"alert_notification_template": "Баланс кошелька '${wallet_id}' меньше ${wallet_amount_threshold}! Текущее значение: {{ $value }}",
|
||||
"parameters": [
|
||||
{
|
||||
"id": 1,
|
||||
"substitution_name": "wallet_id",
|
||||
"readable_name": "Введите идентификатор кошелька",
|
||||
"mandatory": true,
|
||||
"multiple_values": false,
|
||||
"dictionary_name": "wallets"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"substitution_name": "wallet_amount_threshold",
|
||||
"readable_name": "Введите сумму денег на кошельке в минорных единицах, при достижении которой необходимо прислать алерт",
|
||||
"mandatory": true,
|
||||
"multiple_values": false,
|
||||
"regexp": "^\\d+$"
|
||||
}
|
||||
]
|
||||
}
|
62
src/main/resources/template/withdrawal_conversion.json
Normal file
62
src/main/resources/template/withdrawal_conversion.json
Normal file
@ -0,0 +1,62 @@
|
||||
{
|
||||
"id": "withdrawal_conversion",
|
||||
"readable_name": "Конверсия выплат",
|
||||
"prometheus_query": "round(100 * sum(ebm_withdrawals_status_count{provider_id=~\"${provider_id}\", terminal_id=~\"${terminal_id}\",wallet_id=~\"${wallet_id}\",currency=~\"${currency}\",duration=\"${aggr_period}\",status=\"succeeded\"}) / sum(ebm_withdrawals_status_count{provider_id=~\"${provider_id}\",terminal_id=~\"${terminal_id}\",wallet_id=~\"${wallet_id}\",currency=~\"${currency}\",duration=\"${aggr_period}\"}), 1) ${boundary_type} ${conversion_rate_threshold}",
|
||||
"alert_name_template": "Конверсия выплат по провайдеру '${provider_id}', терминалу '${terminal_id}', валюте '${currency}' и кошельку '${wallet_id}' за период: ${aggr_period} ${boundary_type} ${conversion_rate_threshold}%",
|
||||
"alert_notification_template": "Конверсия выплат по провайдеру '${provider_id}', терминалу '${terminal_id}', валюте '${currency}' и кошельку '${wallet_id}' за период: ${aggr_period} ${boundary_type} ${conversion_rate_threshold}%! Текущее значение: {{ $value }}%",
|
||||
"parameters": [
|
||||
{
|
||||
"id": 1,
|
||||
"substitution_name": "provider_id",
|
||||
"readable_name": "Введите идентификатор провайдера (опционально)",
|
||||
"mandatory": false,
|
||||
"multiple_values": true,
|
||||
"dictionary_name": "payout_providers"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"substitution_name": "terminal_id",
|
||||
"readable_name": "Введите идентификатор терминала (опционально)",
|
||||
"mandatory": false,
|
||||
"multiple_values": true,
|
||||
"dictionary_name": "payout_terminals"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"substitution_name": "wallet_id",
|
||||
"readable_name": "Введите идентификатор кошелька (опционально)",
|
||||
"mandatory": false,
|
||||
"multiple_values": true,
|
||||
"dictionary_name": "wallets"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"substitution_name": "currency",
|
||||
"readable_name": "Введите код валюты (опционально)",
|
||||
"mandatory": false,
|
||||
"multiple_values": false,
|
||||
"dictionary_name": "currencies"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"substitution_name": "boundary_type",
|
||||
"readable_name": "Выберите тип условия для алерта",
|
||||
"mandatory": true,
|
||||
"dictionary_name": "boundaries"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"substitution_name": "conversion_rate_threshold",
|
||||
"readable_name": "Введите порог конверсии в процентах (Пример: 85)",
|
||||
"mandatory": true,
|
||||
"regexp": "^[0-9][0-9]?$|^100$"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"substitution_name": "aggr_period",
|
||||
"readable_name": "Период в минутах, за который необходимо проверять количество конверсию (Пример: 60)",
|
||||
"mandatory": true,
|
||||
"dictionary_name": "aggregation_intervals"
|
||||
}
|
||||
]
|
||||
}
|
83
src/main/resources/template/withdrawal_limit_custom.json
Normal file
83
src/main/resources/template/withdrawal_limit_custom.json
Normal file
@ -0,0 +1,83 @@
|
||||
{
|
||||
"id": "payout_limit_custom_hours",
|
||||
"readable_name": "Оборот (выплаты) в определенные часы",
|
||||
"prometheus_query": "(sum(ebm_withdrawals_amount{provider_id=~\"${provider_id}\",wallet_id=~\"${wallet_id}\",status=\"succeeded\",terminal_id=~\"${terminal_id}\",currency=~\"${currency}\",duration=\"${aggr_period}\"}) OR on() vector(0)) ${boundary_type} ${amount_threshold} ${time_interval_boundary} hour() >= ${time_interval_start_hour} < ${time_interval_end_hour}",
|
||||
"alert_name_template": "Оборот выплат по провайдеру '${provider_id}', терминалу '${terminal_id}' и кошельку '${wallet_id}' с валютой '${currency}' за период: ${aggr_period} ${boundary_type} ${amount_threshold}. Временной интервал с ${time_interval_start_hour} UTC по ${time_interval_end_hour} UTC",
|
||||
"alert_notification_template": "Оборот выплат по провайдеру '${provider_id}', терминалу '${terminal_id}' и кошельку '${wallet_id}' с валютой '${currency}' в интервале с ${time_interval_start_hour} UTC по ${time_interval_end_hour} UTC за период: ${aggr_period} ${boundary_type} ${amount_threshold}! Текущее значение: {{ $value }}",
|
||||
"parameters": [
|
||||
{
|
||||
"id": 1,
|
||||
"substitution_name": "provider_id",
|
||||
"readable_name": "Введите идентификатор провайдера (опционально)",
|
||||
"mandatory": false,
|
||||
"multiple_values": true,
|
||||
"dictionary_name": "payout_providers"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"substitution_name": "terminal_id",
|
||||
"readable_name": "Введите идентификатор терминала (опционально)",
|
||||
"mandatory": false,
|
||||
"multiple_values": true,
|
||||
"dictionary_name": "payout_terminals"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"substitution_name": "wallet_id",
|
||||
"readable_name": "Введите идентификатор кошелька (опционально)",
|
||||
"mandatory": false,
|
||||
"multiple_values": true,
|
||||
"dictionary_name": "wallets"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"substitution_name": "currency",
|
||||
"readable_name": "Введите код валюты (опционально)",
|
||||
"mandatory": false,
|
||||
"multiple_values": true,
|
||||
"dictionary_name": "currencies"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"substitution_name": "aggr_period",
|
||||
"readable_name": "Период, за который необходимо считать сумму по всем успешным выплатам",
|
||||
"mandatory": true,
|
||||
"dictionary_name": "aggregation_intervals"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"substitution_name": "amount_threshold",
|
||||
"readable_name": "Введите пороговое значение суммы",
|
||||
"mandatory": true,
|
||||
"regexp": "^\\d+$"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"substitution_name": "boundary_type",
|
||||
"readable_name": "Выберите тип условия для алерта",
|
||||
"mandatory": true,
|
||||
"dictionary_name": "boundaries"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"substitution_name": "time_interval_start_hour",
|
||||
"readable_name": "Введите начало временного интервала в часовом поясе UTC (часы в формате 0-23), когда необходимо проверять сумму. Если вы хотите проверять сумму платежей на протяжении всего дня, введите 0.",
|
||||
"mandatory": true,
|
||||
"regexp": "^2[0-3]|1[0-9]|[0-9]"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"substitution_name": "time_interval_end_hour",
|
||||
"readable_name": "Введите конец временного интервала в часовом поясе UTC (часы в формате 0-23), когда необходимо проверять сумму. Если вы хотите проверять сумму платежей на протяжении всего дня, введите 0.",
|
||||
"mandatory": true,
|
||||
"regexp": "^2[0-3]|1[0-9]|[0-9]"
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"substitution_name": "time_interval_boundary",
|
||||
"readable_name": "Временной интервал, указанный вами, захватывает несколько календарных дней/целый календарный день?",
|
||||
"mandatory": true,
|
||||
"dictionary_name": "time_interval_boundaries"
|
||||
}
|
||||
]
|
||||
}
|
55
src/main/resources/template/withdrawal_transactions.json
Normal file
55
src/main/resources/template/withdrawal_transactions.json
Normal file
@ -0,0 +1,55 @@
|
||||
{
|
||||
"id": "withdrawal_transactions",
|
||||
"readable_name": "Количество выплатных транзакций",
|
||||
"prometheus_query": "(sum(ebm_withdrawals_status_count{terminal_id=~\"${terminal_id}\",provider_id=~\"${provider_id}\",wallet_id=~\"${wallet_id}\",duration=\"${aggr_period}\"}) OR on() vector(0)) ${boundary_type} ${transaction_num_threshold}",
|
||||
"alert_name_template": "Количество выплатных транзакций у провайдера ${provider_id}, терминала ${terminal_id} и кошелька ${wallet_id} за период: ${aggr_period} ${boundary_type} ${transaction_num_threshold}",
|
||||
"alert_notification_template": "Количество выплатных транзакций у провайдера ${provider_id}, терминала ${terminal_id} и кошелька ${wallet_id} за период: ${aggr_period} ${boundary_type} ${transaction_num_threshold}! Текущее значение: {{ $value }}",
|
||||
"parameters": [
|
||||
{
|
||||
"id": 1,
|
||||
"substitution_name": "provider_id",
|
||||
"readable_name": "Введите идентификатор провайдера (опционально)",
|
||||
"mandatory": false,
|
||||
"multiple_values": true,
|
||||
"dictionary_name": "payout_providers"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"substitution_name": "terminal_id",
|
||||
"readable_name": "Введите идентификатор терминала (опционально)",
|
||||
"mandatory": false,
|
||||
"multiple_values": true,
|
||||
"dictionary_name": "payout_terminals"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"substitution_name": "wallet_id",
|
||||
"readable_name": "Введите идентификатор кошелька (опционально)",
|
||||
"mandatory": false,
|
||||
"multiple_values": true,
|
||||
"dictionary_name": "wallets"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"substitution_name": "boundary_type",
|
||||
"readable_name": "Выберите тип условия для алерта",
|
||||
"mandatory": true,
|
||||
"dictionary_name": "boundaries"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"substitution_name": "transaction_num_threshold",
|
||||
"readable_name": "Введите пороговое количество транзакций (Пример: 10000)",
|
||||
"mandatory": true,
|
||||
"regexp": "^\\d+$"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"substitution_name": "aggr_period",
|
||||
"readable_name": "Период, за который необходимо проверять количество транзакций (Пример: 60)",
|
||||
"mandatory": true,
|
||||
"dictionary_name": "aggregation_intervals"
|
||||
}
|
||||
]
|
||||
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
package dev.vality.alerting.mayday.client;
|
||||
|
||||
import dev.vality.alerting.mayday.alertmanager.client.k8s.AlertmanagerClient;
|
||||
import dev.vality.alerting.mayday.alertmanager.client.k8s.model.AlertmanagerConfig;
|
||||
import dev.vality.alerting.mayday.alertmanager.client.k8s.model.AlertmanagerConfigSpec;
|
||||
import io.fabric8.kubernetes.api.model.ObjectMeta;
|
||||
import io.fabric8.kubernetes.client.Config;
|
||||
import io.fabric8.kubernetes.client.ConfigBuilder;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
|
||||
@Disabled("For local client testing")
|
||||
class AlertmanagerClientDebugTest {
|
||||
|
||||
private static final String alertmanagerConfigName = "testconfig";
|
||||
private static final String receiverName = "test_receiver";
|
||||
private static final String webhookUrl = "https://webhook.site/e27da8da-2f80-4ecd-b494-fbe15c84b70f";
|
||||
private final Config config = new ConfigBuilder().withDefaultNamespace().build();
|
||||
private final AlertmanagerClient client = new AlertmanagerClient(config);
|
||||
private final String alertmanagerReceiverName = "mayday-managed-config";
|
||||
|
||||
@Test
|
||||
void getAlertmanagerConfig() {
|
||||
var response = client.getAlertmanagerConfig(alertmanagerConfigName);
|
||||
System.out.println(response);
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteAlertmanagerConfig() {
|
||||
//client.deleteAlertmanagerConfig(alertmanagerConfigName);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createAlertmanagerConfig() {
|
||||
var metadata = new ObjectMeta();
|
||||
metadata.setLabels(Map.of("alerting-stage", "test"));
|
||||
metadata.setName(alertmanagerConfigName);
|
||||
metadata.setLabels(
|
||||
Map.of("service", "mayday")
|
||||
);
|
||||
AlertmanagerConfig alertmanagerConfig = new AlertmanagerConfig();
|
||||
alertmanagerConfig.setMetadata(metadata);
|
||||
AlertmanagerConfigSpec.Receiver receiver = new AlertmanagerConfigSpec.Receiver();
|
||||
AlertmanagerConfigSpec.WebhookConfig webhookConfig = new AlertmanagerConfigSpec.WebhookConfig();
|
||||
webhookConfig.setUrl(webhookUrl);
|
||||
receiver.setName(alertmanagerReceiverName);
|
||||
receiver.setWebhookConfigs(Set.of(webhookConfig));
|
||||
AlertmanagerConfigSpec alertmanagerConfigSpec = new AlertmanagerConfigSpec();
|
||||
alertmanagerConfigSpec.setReceivers(Set.of(receiver));
|
||||
alertmanagerConfig.setSpec(alertmanagerConfigSpec);
|
||||
alertmanagerConfigSpec.setRoute(new AlertmanagerConfigSpec.Route());
|
||||
AlertmanagerConfigSpec.ChildRoute route = new AlertmanagerConfigSpec.ChildRoute();
|
||||
route.setReceiver(alertmanagerReceiverName);
|
||||
route.setGroupBy(Set.of("'...'"));
|
||||
alertmanagerConfigSpec.getRoute().setRoutes(Set.of(route));
|
||||
client.createAlertmanagerConfig(alertmanagerConfig);
|
||||
}
|
||||
|
||||
@Test
|
||||
void addReceiverIfNotExists() {
|
||||
AlertmanagerConfigSpec.ChildRoute route = new AlertmanagerConfigSpec.ChildRoute();
|
||||
route.setReceiver(receiverName);
|
||||
route.setRepeatInterval("5m");
|
||||
|
||||
AlertmanagerConfigSpec.Receiver receiver = new AlertmanagerConfigSpec.Receiver();
|
||||
client.addRouteIfNotExists(alertmanagerConfigName, route);
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteReceivers() {
|
||||
client.deleteRoutes(alertmanagerConfigName, receiverName);
|
||||
}
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
package dev.vality.alerting.mayday.client;
|
||||
|
||||
import dev.vality.alerting.mayday.prometheus.client.k8s.PrometheusClient;
|
||||
import dev.vality.alerting.mayday.prometheus.client.k8s.model.PrometheusRule;
|
||||
import dev.vality.alerting.mayday.prometheus.client.k8s.model.PrometheusRuleSpec;
|
||||
import io.fabric8.kubernetes.api.model.ObjectMeta;
|
||||
import io.fabric8.kubernetes.client.Config;
|
||||
import io.fabric8.kubernetes.client.ConfigBuilder;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
@Disabled("For local client testing")
|
||||
class PrometheusClientDebugTest {
|
||||
|
||||
private final Config config = new ConfigBuilder().withNamespace("default").build();
|
||||
private final PrometheusClient client = new PrometheusClient(config);
|
||||
private final String ruleName = "testrule";
|
||||
private final String groupName = "testGroup";
|
||||
private final String alertName = "unittest_alert";
|
||||
|
||||
@Test
|
||||
void createPrometheusRule() {
|
||||
client.createPrometheusRule(createTestPrometheusRule());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getPrometheusRule() {
|
||||
Optional<PrometheusRule> rule = client.getPrometheusRule(ruleName);
|
||||
assertTrue(rule.isEmpty());
|
||||
createPrometheusRule();
|
||||
rule = client.getPrometheusRule(ruleName);
|
||||
assertTrue(rule.isPresent());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getPrometheusRuleGroupAlerts() {
|
||||
addAlertToPrometheusRuleGroup();
|
||||
var alerts = client.getPrometheusRuleGroupAlerts(ruleName, groupName);
|
||||
assertEquals(1, alerts.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void deletePrometheusRuleGroup() {
|
||||
createPrometheusRule();
|
||||
addAlertToPrometheusRuleGroup();
|
||||
client.deletePrometheusRuleGroup(ruleName, groupName);
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteAlertFromPrometheusRuleGroup() {
|
||||
createPrometheusRule();
|
||||
addAlertToPrometheusRuleGroup();
|
||||
client.deleteAlertFromPrometheusRuleGroup(ruleName, groupName, ruleName);
|
||||
}
|
||||
|
||||
@Test
|
||||
void addAlertToPrometheusRuleGroup() {
|
||||
PrometheusRuleSpec.Rule rule = new PrometheusRuleSpec.Rule();
|
||||
rule.setAlert(alertName);
|
||||
rule.setExpr("vector(1)");
|
||||
rule.setAnnotations(Map.of("readable_name", "тестовый алерт"));
|
||||
client.addAlertToPrometheusRuleGroup(ruleName, groupName, rule);
|
||||
}
|
||||
|
||||
private PrometheusRule createTestPrometheusRule() {
|
||||
PrometheusRule rule = new PrometheusRule();
|
||||
rule.setApiVersion("monitoring.coreos.com/v1");
|
||||
rule.setKind("PrometheusRule");
|
||||
var metadata = new ObjectMeta();
|
||||
metadata.setLabels(Map.of("prometheus", "prometheus"));
|
||||
metadata.setName(ruleName);
|
||||
rule.setMetadata(metadata);
|
||||
PrometheusRuleSpec spec = new PrometheusRuleSpec();
|
||||
rule.setSpec(spec);
|
||||
return rule;
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
package dev.vality.alerting.mayday.controller;
|
||||
|
||||
import dev.vality.alerting.mayday.MaydayApplication;
|
||||
import dev.vality.alerting.mayday.alertmanager.service.AlertmanagerService;
|
||||
import dev.vality.alerting.tg_bot.NotifierServiceSrv;
|
||||
import org.apache.thrift.TException;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.boot.test.web.server.LocalServerPort;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
|
||||
properties = {"spring.flyway.enabled=false"})
|
||||
@ContextConfiguration(classes = {MaydayApplication.class})
|
||||
class WebhookControllerTest {
|
||||
|
||||
@LocalServerPort
|
||||
protected int localServerPort;
|
||||
@MockBean
|
||||
private NotifierServiceSrv.Iface telegramBotClient;
|
||||
@MockBean
|
||||
private AlertmanagerService alertmanagerService;
|
||||
private final RestTemplate restTemplateToService = new RestTemplate();
|
||||
|
||||
@AfterEach
|
||||
void checkMocks() {
|
||||
verifyNoMoreInteractions(telegramBotClient, alertmanagerService);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void alertmanagerWebhook() throws URISyntaxException, IOException, TException {
|
||||
ClassLoader classLoader = getClass().getClassLoader();
|
||||
var path = Paths.get(classLoader.getResource("webhook_example.json").toURI());
|
||||
String request = Files.readString(path, StandardCharsets.UTF_8);
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
|
||||
when(alertmanagerService.containsUserRoute(any(), any())).thenReturn(true);
|
||||
HttpEntity<String> entity = new HttpEntity<>(request, headers);
|
||||
var result = restTemplateToService.exchange(String.format("http://localhost:%s/alertmanager/webhook",
|
||||
localServerPort), HttpMethod.POST, entity, String.class);
|
||||
assertTrue(result.getStatusCode().is2xxSuccessful());
|
||||
verify(telegramBotClient, times(1)).notify(any());
|
||||
verify(alertmanagerService, times(1)).containsUserRoute(any(), any());
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,159 @@
|
||||
package dev.vality.alerting.mayday.integration;
|
||||
|
||||
import dev.vality.alerting.mayday.Alert;
|
||||
import dev.vality.alerting.mayday.AlertConfiguration;
|
||||
import dev.vality.alerting.mayday.AlertingServiceSrv;
|
||||
import dev.vality.alerting.mayday.UserAlert;
|
||||
import dev.vality.alerting.mayday.alertmanager.client.k8s.AlertmanagerClient;
|
||||
import dev.vality.alerting.mayday.alertmanager.client.k8s.model.AlertmanagerConfig;
|
||||
import dev.vality.alerting.mayday.alertmanager.service.AlertmanagerService;
|
||||
import dev.vality.alerting.mayday.alerttemplate.dao.DawayDao;
|
||||
import dev.vality.alerting.mayday.common.constant.PrometheusRuleAnnotation;
|
||||
import dev.vality.alerting.mayday.prometheus.client.k8s.PrometheusClient;
|
||||
import dev.vality.alerting.mayday.prometheus.client.k8s.model.PrometheusRule;
|
||||
import dev.vality.alerting.mayday.prometheus.service.PrometheusService;
|
||||
import dev.vality.alerting.mayday.testutil.DawayObjectUtil;
|
||||
import dev.vality.alerting.mayday.testutil.K8sObjectUtil;
|
||||
import dev.vality.alerting.mayday.testutil.ThriftObjectUtil;
|
||||
import dev.vality.testcontainers.annotations.DefaultSpringBootTest;
|
||||
import org.apache.thrift.TException;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@DefaultSpringBootTest
|
||||
public class AlertingIntegrationTest {
|
||||
|
||||
@Autowired
|
||||
private AlertingServiceSrv.Iface thriftEndpoint;
|
||||
@Autowired
|
||||
private PrometheusService prometheusService;
|
||||
@Autowired
|
||||
private AlertmanagerService alertmanagerService;
|
||||
|
||||
@MockBean
|
||||
private PrometheusClient prometheusClient;
|
||||
@MockBean
|
||||
private AlertmanagerClient alertmanagerClient;
|
||||
@MockBean
|
||||
private DawayDao dawayDao;
|
||||
|
||||
private AutoCloseable mocks;
|
||||
|
||||
private Object[] preparedMocks;
|
||||
|
||||
@BeforeEach
|
||||
public void init() {
|
||||
mocks = MockitoAnnotations.openMocks(this);
|
||||
preparedMocks = new Object[]{prometheusClient, alertmanagerClient, dawayDao};
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void clean() throws Exception {
|
||||
verifyNoMoreInteractions(preparedMocks);
|
||||
mocks.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void createAlert() throws TException {
|
||||
when(prometheusClient.getPrometheusRule(prometheusService.getPrometheusRuleName()))
|
||||
.thenReturn(Optional.of(new PrometheusRule()));
|
||||
when(alertmanagerClient.getAlertmanagerConfig(alertmanagerService.getAlertmanagerConfigName()))
|
||||
.thenReturn(Optional.of(new AlertmanagerConfig()));
|
||||
when(dawayDao.getPaymentProviders()).thenReturn(DawayObjectUtil.getTestProviders());
|
||||
when(dawayDao.getPaymentTerminals()).thenReturn(DawayObjectUtil.getTestTerminals());
|
||||
when(dawayDao.getShops()).thenReturn(DawayObjectUtil.getTestShops());
|
||||
when(dawayDao.getCurrencies()).thenReturn(DawayObjectUtil.getTestCurrencies());
|
||||
|
||||
var createAlertRequest =
|
||||
ThriftObjectUtil.testCreatePaymentConversionAlertRequest(getPaymentConversionAlertConfiguration());
|
||||
thriftEndpoint.createAlert(createAlertRequest);
|
||||
verify(prometheusClient, times(1)).getPrometheusRule(prometheusService.getPrometheusRuleName());
|
||||
verify(prometheusClient, times(1))
|
||||
.addAlertToPrometheusRuleGroup(eq(prometheusService.getPrometheusRuleName()),
|
||||
eq(createAlertRequest.getUserId()), any());
|
||||
verify(alertmanagerClient, times(1))
|
||||
.getAlertmanagerConfig(eq(alertmanagerService.getAlertmanagerConfigName()));
|
||||
verify(alertmanagerClient, times(1))
|
||||
.addRouteIfNotExists(eq(alertmanagerService.getAlertmanagerConfigName()), any());
|
||||
|
||||
verify(dawayDao, times(2)).getPaymentProviders();
|
||||
verify(dawayDao, times(2)).getPaymentTerminals();
|
||||
verify(dawayDao, times(2)).getShops();
|
||||
verify(dawayDao, times(2)).getCurrencies();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getUserAlertsEmpty() throws TException {
|
||||
String userName = UUID.randomUUID().toString();
|
||||
when(prometheusClient.getPrometheusRule(prometheusService.getPrometheusRuleName()))
|
||||
.thenReturn(Optional.of(new PrometheusRule()));
|
||||
when(prometheusClient.getPrometheusRuleGroupAlerts(prometheusService.getPrometheusRuleName(), userName))
|
||||
.thenReturn(Set.of());
|
||||
List<UserAlert> userAlerts = thriftEndpoint.getUserAlerts(userName);
|
||||
assertNotNull(userAlerts);
|
||||
assertTrue(userAlerts.isEmpty());
|
||||
verify(prometheusClient, times(1))
|
||||
.getPrometheusRule(prometheusService.getPrometheusRuleName());
|
||||
verify(prometheusClient, times(1))
|
||||
.getPrometheusRuleGroupAlerts(prometheusService.getPrometheusRuleName(), userName);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getUserAlerts() throws TException {
|
||||
String userName = UUID.randomUUID().toString();
|
||||
var testRule = K8sObjectUtil.testPrometheusRule();
|
||||
when(prometheusClient.getPrometheusRule(prometheusService.getPrometheusRuleName()))
|
||||
.thenReturn(Optional.of(new PrometheusRule()));
|
||||
when(prometheusClient.getPrometheusRuleGroupAlerts(prometheusService.getPrometheusRuleName(), userName))
|
||||
.thenReturn(Set.of(testRule));
|
||||
List<UserAlert> userAlerts = thriftEndpoint.getUserAlerts(userName);
|
||||
assertNotNull(userAlerts);
|
||||
assertEquals(1, userAlerts.size());
|
||||
|
||||
UserAlert userAlert = userAlerts.get(0);
|
||||
assertEquals(testRule.getAlert(), userAlert.getId());
|
||||
assertEquals(testRule.getAnnotations().get(PrometheusRuleAnnotation.ALERT_NAME), userAlert.getName());
|
||||
verify(prometheusClient, times(1))
|
||||
.getPrometheusRule(prometheusService.getPrometheusRuleName());
|
||||
verify(prometheusClient, times(1))
|
||||
.getPrometheusRuleGroupAlerts(prometheusService.getPrometheusRuleName(), userName);
|
||||
}
|
||||
|
||||
AlertConfiguration getPaymentConversionAlertConfiguration() throws TException {
|
||||
List<Alert> alertList = getSupportedAlerts();
|
||||
AlertConfiguration alertConfiguration =
|
||||
thriftEndpoint.getAlertConfiguration(alertList.stream()
|
||||
.filter(alert -> alert.getId().equals("payment_conversion"))
|
||||
.findFirst()
|
||||
.orElseThrow().getId());
|
||||
assertNotNull(alertConfiguration);
|
||||
assertNotNull(alertConfiguration.getId());
|
||||
assertNotNull(alertConfiguration.getParameters());
|
||||
assertFalse(alertConfiguration.getParameters().isEmpty());
|
||||
return alertConfiguration;
|
||||
}
|
||||
|
||||
private List<Alert> getSupportedAlerts() throws TException {
|
||||
List<Alert> alertList = thriftEndpoint.getSupportedAlerts();
|
||||
assertNotNull(alertList);
|
||||
assertFalse(alertList.isEmpty());
|
||||
|
||||
for (Alert alert : alertList) {
|
||||
assertNotNull(alert.getId());
|
||||
assertNotNull(alert.getName());
|
||||
}
|
||||
return alertList;
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
package dev.vality.alerting.mayday.testutil;
|
||||
|
||||
import dev.vality.alerting.mayday.alerttemplate.model.daway.Currency;
|
||||
import dev.vality.alerting.mayday.alerttemplate.model.daway.Provider;
|
||||
import dev.vality.alerting.mayday.alerttemplate.model.daway.Shop;
|
||||
import dev.vality.alerting.mayday.alerttemplate.model.daway.Terminal;
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@UtilityClass
|
||||
public class DawayObjectUtil {
|
||||
|
||||
public static List<Provider> getTestProviders() {
|
||||
return List.of(
|
||||
Provider.builder()
|
||||
.id(1)
|
||||
.name("test").build()
|
||||
);
|
||||
}
|
||||
|
||||
public static List<Terminal> getTestTerminals() {
|
||||
return List.of(
|
||||
Terminal.builder()
|
||||
.id(1)
|
||||
.name("test").build()
|
||||
);
|
||||
}
|
||||
|
||||
public static List<Shop> getTestShops() {
|
||||
return List.of(
|
||||
Shop.builder()
|
||||
.id("def91399-75ff-4307-8634-626c85859ea4")
|
||||
.name("test").build()
|
||||
);
|
||||
}
|
||||
|
||||
public static List<Currency> getTestCurrencies() {
|
||||
return List.of(
|
||||
Currency.builder()
|
||||
.symbolicCode("RUB")
|
||||
.name("Рублик").build()
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package dev.vality.alerting.mayday.testutil;
|
||||
|
||||
import dev.vality.alerting.mayday.prometheus.client.k8s.model.PrometheusRuleSpec;
|
||||
import dev.vality.alerting.mayday.common.constant.PrometheusRuleAnnotation;
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@UtilityClass
|
||||
public class K8sObjectUtil {
|
||||
|
||||
public static PrometheusRuleSpec.Rule testPrometheusRule() {
|
||||
var rule = new PrometheusRuleSpec.Rule();
|
||||
rule.setAlert("test_alert");
|
||||
rule.setExpr("vector(1)");
|
||||
rule.setAnnotations(Map.of(PrometheusRuleAnnotation.ALERT_NAME, "тестовый алерт"));
|
||||
return rule;
|
||||
}
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
package dev.vality.alerting.mayday.testutil;
|
||||
|
||||
import dev.vality.alerting.mayday.AlertConfiguration;
|
||||
import dev.vality.alerting.mayday.CreateAlertRequest;
|
||||
import dev.vality.alerting.mayday.ParameterInfo;
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@UtilityClass
|
||||
public class ThriftObjectUtil {
|
||||
|
||||
public static CreateAlertRequest testCreatePaymentConversionAlertRequest(AlertConfiguration alertConfiguration) {
|
||||
var request = new CreateAlertRequest();
|
||||
request.setAlertId(alertConfiguration.getId());
|
||||
request.setUserId(UUID.randomUUID().toString());
|
||||
List<ParameterInfo> parameters = new ArrayList<>();
|
||||
|
||||
var providerParameter = new ParameterInfo();
|
||||
providerParameter.setId("1");
|
||||
providerParameter.setValue("(1) test");
|
||||
parameters.add(providerParameter);
|
||||
|
||||
var terminalParameter = new ParameterInfo();
|
||||
terminalParameter.setId("2");
|
||||
terminalParameter.setValue("(1) test");
|
||||
parameters.add(terminalParameter);
|
||||
|
||||
var shopParameter = new ParameterInfo();
|
||||
shopParameter.setId("3");
|
||||
shopParameter.setValue("(def91399) test");
|
||||
parameters.add(shopParameter);
|
||||
|
||||
var currencyParameter = new ParameterInfo();
|
||||
currencyParameter.setId("4");
|
||||
currencyParameter.setValue("(RUB) Рублик");
|
||||
parameters.add(currencyParameter);
|
||||
|
||||
var boundaryParameter = new ParameterInfo();
|
||||
boundaryParameter.setId("5");
|
||||
boundaryParameter.setValue("Больше порогового значения");
|
||||
parameters.add(boundaryParameter);
|
||||
|
||||
var thresholdParameter = new ParameterInfo();
|
||||
thresholdParameter.setId("6");
|
||||
thresholdParameter.setValue("10");
|
||||
parameters.add(thresholdParameter);
|
||||
|
||||
var periodParameter = new ParameterInfo();
|
||||
periodParameter.setId("7");
|
||||
periodParameter.setValue("15 минут");
|
||||
parameters.add(periodParameter);
|
||||
|
||||
var ruleCheckDurationParameter = new ParameterInfo();
|
||||
ruleCheckDurationParameter.setId(String.valueOf(Integer.MAX_VALUE));
|
||||
ruleCheckDurationParameter.setValue("10");
|
||||
parameters.add(ruleCheckDurationParameter);
|
||||
|
||||
var alertRepeatParameter = new ParameterInfo();
|
||||
alertRepeatParameter.setId(String.valueOf(Integer.MAX_VALUE - 1));
|
||||
alertRepeatParameter.setValue("10");
|
||||
parameters.add(alertRepeatParameter);
|
||||
|
||||
request.setParameters(parameters);
|
||||
return request;
|
||||
}
|
||||
}
|
10
src/test/resources/logback-test.xml
Normal file
10
src/test/resources/logback-test.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
|
||||
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
|
||||
|
||||
<root level="info">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
</root>
|
||||
<logger name="dev.vality.woody" level="ALL"/>
|
||||
</configuration>
|
35
src/test/resources/webhook_example.json
Normal file
35
src/test/resources/webhook_example.json
Normal file
@ -0,0 +1,35 @@
|
||||
{
|
||||
"receiver": "webhook",
|
||||
"status": "firing",
|
||||
"alerts": [
|
||||
{
|
||||
"status": "firing",
|
||||
"labels": {
|
||||
"alertname": "unittest_alert",
|
||||
"prometheus": "default/prometheus"
|
||||
},
|
||||
"annotations": {
|
||||
"readable_name": "тестовый алерт",
|
||||
"alert_description": "что-то случилось :("
|
||||
},
|
||||
"startsAt": "2023-06-14T11:00:52.059Z",
|
||||
"endsAt": "0001-01-01T00:00:00Z",
|
||||
"generatorURL": "http://prometheus-prometheus-0:9090/graph?g0.expr=vector%281%29\u0026g0.tab=1",
|
||||
"fingerprint": "9f370ce461911051"
|
||||
}
|
||||
],
|
||||
"groupLabels": {
|
||||
"alertname": "unittest_alert"
|
||||
},
|
||||
"commonLabels": {
|
||||
"alertname": "unittest_alert",
|
||||
"prometheus": "default/prometheus"
|
||||
},
|
||||
"commonAnnotations": {
|
||||
"readable_name": "тестовый алерт"
|
||||
},
|
||||
"externalURL": "http://alertmanager-alertmanager-0:9093",
|
||||
"version": "4",
|
||||
"groupKey": "{}:{alertname=\"unittest_alert\"}",
|
||||
"truncatedAlerts": 0
|
||||
}
|
Loading…
Reference in New Issue
Block a user