BUS-97: user friendly values (#24)

* BUS-97: user friendly values

* BUS-97: user friendly values
This commit is contained in:
Fedor Shimich 2024-04-26 11:59:56 +03:00 committed by GitHub
parent 413facf1f7
commit 582821a671
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 217 additions and 71 deletions

View File

@ -68,7 +68,8 @@ public class AlertmanagerService {
route.setGroupWait(ONE_SEC_WAIT);
String repeatInterval = FormatUtil.formatMinutesDuration(createAlertDto.getParameters()
.get(String.valueOf(
AlertConfigurationRequiredParameter.ALERT_REPEAT_MINUTES.getSubstitutionName())).get(0));
AlertConfigurationRequiredParameter.ALERT_REPEAT_MINUTES.getSubstitutionName())).get(0)
.getValue());
route.setGroupInterval(repeatInterval);
var alertnameMatcher =
AlertmanagerFunctionsUtil.createMatcher(PrometheusRuleLabel.ALERT_NAME, createAlertDto.getAlertId());

View File

@ -0,0 +1,22 @@
package dev.vality.alerting.mayday.alerttemplate.model.dictionary;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@Data
@Builder
@AllArgsConstructor
public class DictionaryData {
private String value;
private String userFriendlyValue;
public DictionaryData(String value) {
this.value = value;
}
public String getUserFriendlyValue() {
return userFriendlyValue != null ? userFriendlyValue : value;
}
}

View File

@ -3,6 +3,7 @@ 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 dev.vality.alerting.mayday.alerttemplate.model.dictionary.DictionaryData;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@ -19,7 +20,7 @@ public class DictionaryService {
private final DawayDao dawayDao;
public Map<String, String> getDictionary(DictionaryType type) {
public Map<String, DictionaryData> getDictionary(DictionaryType type) {
return switch (type) {
case TERMINALS -> convertTerminalsToDictionary(dawayDao.getAllTerminals());
case PAYMENT_TERMINALS -> convertTerminalsToDictionary(dawayDao.getPaymentTerminals());
@ -30,50 +31,64 @@ public class DictionaryService {
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", "Текущий " +
"календарный день (MSK)", "today_msk");
case PAYMENT_LIMIT_SCOPES -> Map.of(
"Провайдер", new DictionaryData("provider"),
"Провайдер + терминал", new DictionaryData("provider,terminal"),
"Провайдер + терминал + магазин", new DictionaryData("provider,shop,terminal"),
"Терминал", new DictionaryData("terminal"),
"Магазин", new DictionaryData("shop")
);
case PAYOUT_LIMIT_SCOPES -> Map.of(
"Провайдер", new DictionaryData("provider"),
"Провайдер + терминал", new DictionaryData("provider,terminal"),
"Провайдер + терминал + кошелек", new DictionaryData("provider,terminal,wallet"),
"Терминал", new DictionaryData("terminal"),
"Кошелёк", new DictionaryData("wallet")
);
case CONDITIONAL_BOUNDARIES -> Map.of(
"Больше порогового значения", new DictionaryData(">"),
"Меньше порогового значения", new DictionaryData("<")
);
case TIME_INTERVAL_BOUNDARIES -> Map.of(
"Да", new DictionaryData("unless"),
"Нет", new DictionaryData("and")
);
case AGGREGATION_INTERVALS -> Map.of(
"5 минут", new DictionaryData("5m"),
"15 минут", new DictionaryData("15m"),
"30 минут", new DictionaryData("30m"),
"1 час", new DictionaryData("1h"),
"3 часа", new DictionaryData("3h"),
"6 часов", new DictionaryData("6h"),
"12 часов", new DictionaryData("12h"),
"24 часа", new DictionaryData("24h"),
"Текущий календарный день (MSK)", new DictionaryData("today_msk"));
};
}
private Map<String, String> convertTerminalsToDictionary(List<Terminal> terminals) {
private Map<String, DictionaryData> convertTerminalsToDictionary(List<Terminal> terminals) {
return terminals.stream()
.collect(Collectors.toMap(
terminal -> formatDictionaryKey(Integer.toString(terminal.getId()), terminal.getName()),
terminal -> Integer.toString(terminal.getId())));
.map(terminal -> createDictionaryData(terminal.getId(), terminal.getName()))
.collect(Collectors.toMap(DictionaryData::getUserFriendlyValue, dictionaryData -> dictionaryData));
}
private Map<String, String> convertProvidersToDictionary(List<Provider> providers) {
private Map<String, DictionaryData> convertProvidersToDictionary(List<Provider> providers) {
return providers.stream()
.collect(Collectors.toMap(
provider -> formatDictionaryKey(Integer.toString(provider.getId()), provider.getName()),
provider -> Integer.toString(provider.getId())));
.map(provider -> createDictionaryData(provider.getId(), provider.getName()))
.collect(Collectors.toMap(DictionaryData::getUserFriendlyValue, dictionaryData -> dictionaryData));
}
private Map<String, String> convertWalletsToDictionary(List<Wallet> wallets) {
private Map<String, DictionaryData> convertWalletsToDictionary(List<Wallet> wallets) {
return wallets.stream()
.collect(Collectors.toMap(
wallet -> formatDictionaryKey(wallet.getId(), wallet.getName()),
Wallet::getId));
.map(wallet -> createDictionaryData(wallet.getId(), wallet.getName()))
.collect(Collectors.toMap(DictionaryData::getUserFriendlyValue, dictionaryData -> dictionaryData));
}
private Map<String, String> convertShopsToDictionary(List<Shop> shops) {
return shops.stream()
.collect(Collectors.toMap(
shop -> formatDictionaryKey(formatShopId(shop.getId()), shop.getName()),
Shop::getId));
private Map<String, DictionaryData> convertShopsToDictionary(List<Shop> shops) {
return shops.stream().collect(Collectors.toMap(
shop -> formatDictionaryString(formatShopId(shop.getId()), shop.getName()),
shop -> createDictionaryData(shop.getId(), shop.getName())
));
}
// Возвращаем только часть UUID, т.к иначе строка выходит слишком длинной
@ -87,15 +102,25 @@ public class DictionaryService {
}
}
private Map<String, String> convertCurrenciesToDictionary(List<Currency> currencies) {
private Map<String, DictionaryData> convertCurrenciesToDictionary(List<Currency> currencies) {
return currencies.stream()
.collect(Collectors.toMap(
currency -> formatDictionaryKey(currency.getSymbolicCode(), currency.getName()),
Currency::getSymbolicCode));
.map(currency -> createDictionaryData(currency.getSymbolicCode(), currency.getName()))
.collect(Collectors.toMap(DictionaryData::getUserFriendlyValue, dictionaryData -> dictionaryData));
}
private String formatDictionaryKey(String id, String description) {
private String formatDictionaryString(String id, String description) {
return String.format("(%s) %s", id, description);
}
private DictionaryData createDictionaryData(String id, String description) {
return DictionaryData.builder()
.value(id)
.userFriendlyValue(formatDictionaryString(id, description))
.build();
}
private DictionaryData createDictionaryData(Integer id, String description) {
return createDictionaryData(Integer.toString(id), description);
}
}

View File

@ -4,6 +4,7 @@ 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.model.dictionary.DictionaryData;
import dev.vality.alerting.mayday.alerttemplate.service.DictionaryService;
import dev.vality.alerting.mayday.common.constant.AlertConfigurationRequiredParameter;
import dev.vality.alerting.mayday.common.dto.CreateAlertDto;
@ -18,6 +19,7 @@ import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Slf4j
@Component
@ -25,7 +27,7 @@ import java.util.stream.Collectors;
public class TemplateHelper {
private static final String anyClientValue = "-";
private static final String anyPrometheusValue = ".*";
private static final DictionaryData anyPrometheusValue = new DictionaryData(".*");
private static final String anyUserFriendlyValue = "<любое значение>";
private static final String multiValuePrometheusDelimiter = "|";
private static final String multiValueUserFriendlyDelimiter = ",";
@ -37,7 +39,7 @@ public class TemplateHelper {
List<AlertTemplate.AlertConfigurationParameter>
metricParams) {
Map<String, List<String>> parameters = mergeParameters(createAlertRequest.getParameters(), metricParams);
var parameters = mergeParameters(createAlertRequest.getParameters(), metricParams);
String queryExpression = prepareMetricExpression(metricTemplate, parameters);
log.debug("Prepared prometheus expression: {}", queryExpression);
String alertId = generateAlertId(createAlertRequest, queryExpression);
@ -52,10 +54,10 @@ public class TemplateHelper {
.build();
}
protected Map<String, List<String>> mergeParameters(List<ParameterInfo> externalParamsInfo,
List<AlertTemplate.AlertConfigurationParameter>
maydayParamsInfo) {
Map<String, List<String>> params = maydayParamsInfo.stream()
protected Map<String, List<DictionaryData>> mergeParameters(List<ParameterInfo> externalParamsInfo,
List<AlertTemplate.AlertConfigurationParameter>
maydayParamsInfo) {
Map<String, List<DictionaryData>> params = maydayParamsInfo.stream()
.map(maydayParamInfo -> {
var externalParamInfos = externalParamsInfo.stream()
.filter(userParamInfo ->
@ -65,7 +67,7 @@ public class TemplateHelper {
validateMultipleValues(maydayParamInfo, externalParamInfos);
validateMandatoryValues(maydayParamInfo, externalParamInfos);
List<String> values = new ArrayList<>();
List<DictionaryData> values = new ArrayList<>();
if (!maydayParamInfo.getMandatory() && externalParamInfos.size() == 1) {
values.add(hasNoValue(externalParamInfos.get(0)) ? anyPrometheusValue :
getParameterValue(maydayParamInfo, externalParamInfos.get(0)));
@ -86,14 +88,16 @@ public class TemplateHelper {
Arrays.stream(AlertConfigurationRequiredParameter.values()).forEach(
requiredParameter -> {
var param = getRequiredParameter(String.valueOf(requiredParameter.getId()), externalParamsInfo);
params.put(requiredParameter.getSubstitutionName(), List.of(param.getValue()));
params.put(requiredParameter.getSubstitutionName(),
Stream.of(param.getValue()).map(DictionaryData::new).toList()
);
}
);
return params;
}
private void validateMultipleValues(AlertTemplate.AlertConfigurationParameter maydayParamInfo,
List<ParameterInfo> externalParamInfos) {
List<ParameterInfo> externalParamInfos) {
if (externalParamInfos.size() > 1 && !maydayParamInfo.getMultipleValues()) {
throw new AlertConfigurationException(String.format("Parameter '%s' cannot have " +
"multiple values!", maydayParamInfo.getSubstitutionName()));
@ -122,54 +126,59 @@ public class TemplateHelper {
.getBytes(StandardCharsets.UTF_8));
}
protected String prepareMetricExpression(AlertTemplate metricTemplate, Map<String, List<String>> parameters) {
protected String prepareMetricExpression(AlertTemplate metricTemplate,
Map<String, List<DictionaryData>> parameters) {
return prepareTemplate(metricTemplate.getPrometheusQuery(), parameters);
}
protected String prepareUserFriendlyAlertName(AlertTemplate metricTemplate, Map<String, List<String>> parameters) {
protected String prepareUserFriendlyAlertName(AlertTemplate metricTemplate,
Map<String, List<DictionaryData>> parameters) {
return prepareUserFriendlyTemplate(metricTemplate.getAlertNameTemplate(), parameters);
}
protected String prepareMetricAlertMessage(AlertTemplate metricTemplate, Map<String, List<String>> parameters) {
protected String prepareMetricAlertMessage(AlertTemplate metricTemplate,
Map<String, List<DictionaryData>> parameters) {
return prepareUserFriendlyTemplate(metricTemplate.getAlertNotificationTemplate(), parameters);
}
private String prepareTemplate(String template, Map<String, List<String>> replacements) {
private String prepareTemplate(String template, Map<String, List<DictionaryData>> 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());
for (Map.Entry<String, List<DictionaryData>> entry : replacementsEntries) {
var value = entry.getValue().stream()
.map(DictionaryData::getValue)
.collect(Collectors.joining(multiValuePrometheusDelimiter));
preparedTemplate = preparedTemplate.replace(formatReplacementVariable(entry.getKey()), value);
}
return preparedTemplate;
}
private String prepareUserFriendlyTemplate(String template, Map<String, List<String>> replacements) {
private String prepareUserFriendlyTemplate(String template, Map<String, List<DictionaryData>> 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());
for (Map.Entry<String, List<DictionaryData>> entry : replacementsEntries) {
var value = entry.getValue().stream()
.map(DictionaryData::getUserFriendlyValue)
.map(this::formatAnyValue)
.collect(Collectors.joining(multiValueUserFriendlyDelimiter));
preparedTemplate = preparedTemplate.replace(formatReplacementVariable(entry.getKey()), value);
}
return preparedTemplate;
}
private String formatAnyValue(String value) {
if (anyPrometheusValue.equals(value)) {
if (anyPrometheusValue.getValue().equals(value)) {
return anyUserFriendlyValue;
}
return value;
}
private String getParameterValue(AlertTemplate.AlertConfigurationParameter maydayParamInfo,
ParameterInfo userParamInfo) {
private DictionaryData getParameterValue(AlertTemplate.AlertConfigurationParameter maydayParamInfo,
ParameterInfo userParamInfo) {
if (maydayParamInfo.getDictionaryName() != null) {
return dictionaryService.getDictionary(maydayParamInfo.getDictionaryName()).get(userParamInfo.getValue());
}
return userParamInfo.getValue();
return new DictionaryData(userParamInfo.getValue());
}
private String formatReplacementVariable(String variableName) {

View File

@ -1,5 +1,6 @@
package dev.vality.alerting.mayday.common.dto;
import dev.vality.alerting.mayday.alerttemplate.model.dictionary.DictionaryData;
import lombok.Builder;
import lombok.Data;
@ -14,5 +15,5 @@ public class CreateAlertDto {
private String prometheusQuery;
private String userFriendlyAlertName;
private String userFriendlyAlertDescription;
private Map<String, List<String>> parameters;
private Map<String, List<DictionaryData>> parameters;
}

View File

@ -15,7 +15,7 @@ public class DawayObjectUtil {
return List.of(
Provider.builder()
.id(1)
.name("test").build()
.name("provider").build()
);
}
@ -23,7 +23,7 @@ public class DawayObjectUtil {
return List.of(
Terminal.builder()
.id(1)
.name("test").build()
.name("terminal").build()
);
}
@ -31,7 +31,7 @@ public class DawayObjectUtil {
return List.of(
Shop.builder()
.id("def91399-75ff-4307-8634-626c85859ea4")
.name("test").build()
.name("shop").build()
);
}

View File

@ -20,17 +20,17 @@ public class ThriftObjectUtil {
var providerParameter = new ParameterInfo();
providerParameter.setId("1");
providerParameter.setValue("(1) test");
providerParameter.setValue("(1) provider");
parameters.add(providerParameter);
var terminalParameter = new ParameterInfo();
terminalParameter.setId("2");
terminalParameter.setValue("(1) test");
terminalParameter.setValue("(1) terminal");
parameters.add(terminalParameter);
var shopParameter = new ParameterInfo();
shopParameter.setId("3");
shopParameter.setValue("(def91399) test");
shopParameter.setValue("(def91399) shop");
parameters.add(shopParameter);
var currencyParameter = new ParameterInfo();

View File

@ -0,0 +1,88 @@
package dev.vality.alerting.mayday.unit;
import dev.vality.alerting.mayday.Alert;
import dev.vality.alerting.mayday.AlertConfiguration;
import dev.vality.alerting.mayday.AlertingServiceSrv;
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.alerttemplate.service.TemplateService;
import dev.vality.alerting.mayday.alerttemplate.service.helper.TemplateHelper;
import dev.vality.alerting.mayday.testutil.DawayObjectUtil;
import dev.vality.alerting.mayday.testutil.ThriftObjectUtil;
import dev.vality.testcontainers.annotations.DefaultSpringBootTest;
import org.apache.thrift.TException;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import java.util.List;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.Mockito.when;
@DefaultSpringBootTest
public class TemplateHelperTest {
@Autowired
private AlertingServiceSrv.Iface thriftEndpoint;
@Autowired
private AlertmanagerService alertmanagerService;
@Autowired
private TemplateHelper templateHelper;
@Autowired
private TemplateService templateService;
@MockBean
private AlertmanagerClient alertmanagerClient;
@MockBean
private DawayDao dawayDao;
private static final String prometheusQuery = "round(100 * sum(ebm_payments_status_count{provider_id=~\"1\", " +
"terminal_id=~\"1\",shop_id=~\"def91399-75ff-4307-8634-626c85859ea4\",currency=~\"RUB\",duration=\"15m\"," +
"status=\"captured\"}) / sum(ebm_payments_status_count{provider_id=~\"1\",terminal_id=~\"1\"," +
"shop_id=~\"def91399-75ff-4307-8634-626c85859ea4\",currency=~\"RUB\",duration=\"15m\"}), 1) > 10";
private static final String userFriendlyAlertName = "Конверсия платежей по провайдеру '(1) provider', " +
"терминалу '(1) terminal', валюте '(RUB) Рублик' и магазину " +
"'(def91399-75ff-4307-8634-626c85859ea4) shop' за период: 15m > 10%";
@Test
void preparePrometheusRuleDataTest() throws TException {
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());
var metricParams = templateService.getAlertTemplateParams(createAlertRequest.getAlertId());
var metricTemplate = templateService.getAlertTemplateById(createAlertRequest.getAlertId());
var result = templateHelper.preparePrometheusRuleData(createAlertRequest, metricTemplate, metricParams);
assertNotNull(result);
assertEquals(prometheusQuery, result.getPrometheusQuery());
assertEquals(userFriendlyAlertName, result.getUserFriendlyAlertName());
}
AlertConfiguration getPaymentConversionAlertConfiguration() throws TException {
List<Alert> alertList = thriftEndpoint.getSupportedAlerts();
return
thriftEndpoint.getAlertConfiguration(alertList.stream()
.filter(alert -> alert.getId().equals("payment_conversion"))
.findFirst()
.orElseThrow().getId());
}
}