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:
Egor Cherniak 2023-10-20 16:23:02 +03:00 committed by GitHub
parent 396cce59dd
commit b1bc23ab8f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
87 changed files with 3918 additions and 1 deletions

2
.github/settings.yml vendored Normal file
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1 @@
* @valitydev/java

176
LICENSE Normal file
View 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
View File

@ -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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 321 KiB

BIN
img/docker_k8s.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

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
View 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
View File

@ -0,0 +1,4 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["local>valitydev/.github:renovate-config"]
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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 {
}

View File

@ -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;
}
}

View File

@ -0,0 +1,4 @@
package dev.vality.alerting.mayday.alertmanager.client.k8s.model;
public class AlertmanagerConfigStatus {
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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 = "Более не активно: ";
}

View File

@ -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";
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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();
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,9 @@
package dev.vality.alerting.mayday.alerttemplate.error;
public class AlertTemplateNotFoundException extends RuntimeException {
public AlertTemplateNotFoundException(String message) {
super(message);
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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());
}
}

View File

@ -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();
}
}

View File

@ -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;
}

View File

@ -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";
}

View File

@ -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";
}

View File

@ -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;
}

View File

@ -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);
}
}
}

View File

@ -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 {
}

View File

@ -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;
}
}

View File

@ -0,0 +1,5 @@
package dev.vality.alerting.mayday.prometheus.client.k8s.model;
public class PrometheusRuleStatus {
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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));
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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());
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View 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'

View 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$"
}
]
}

View 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$"
}
]
}

View 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"
}
]
}

View 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"
}
]
}

View 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"
}
]
}

View 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+$"
}
]
}

View 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"
}
]
}

View 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"
}
]
}

View 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"
}
]
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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());
}
}

View File

@ -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;
}
}

View File

@ -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()
);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View 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>

View 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
}