Compare commits

..
Author SHA1 Message Date
kangli 28f5410555 Merge branch 'master' into jackson-prop 2025-07-06 22:54:14 +08:00
553bdbaf9f [refactor] AlarmCacheManager refactoring processing logic (#3525)
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: 淞筱 <2030509072@qq.com>
Co-authored-by: aias00 <liuhongyu@apache.org>
Co-authored-by: shown <yuluo08290126@gmail.com>
Co-authored-by: Sherlock Yin <sherlock.yin1994@gmail.com>
Co-authored-by: kangli <likang@apache.org>
2025-07-06 22:06:30 +08:00
40d3f1243b feat: Add LogUtil wrapper and optimize logging comments (#3489)
Co-authored-by: aias00 <liuhongyu@apache.org>
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: shown <yuluo08290126@gmail.com>
Co-authored-by: kangli <likang@apache.org>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-07-06 20:57:39 +08:00
740e3f8385 [feature] Support export all allmonitors (#3509)
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: aias00 <liuhongyu@apache.org>
Co-authored-by: Logic <zqr10159@dromara.org>
Co-authored-by: shown <yuluo08290126@gmail.com>
Co-authored-by: kangli <likang@apache.org>
2025-07-06 19:06:19 +08:00
tomsun28 d5d95307ba Merge branch 'master' into jackson-prop 2025-07-06 15:12:57 +08:00
Calvin 47d089dff7 [doc] japanese kafka client (#3550) 2025-07-06 06:51:49 +08:00
26 changed files with 793 additions and 1662 deletions
@@ -17,6 +17,9 @@
package org.apache.hertzbeat.alert.calculate;
import com.google.common.collect.Table;
import com.google.common.collect.Tables;
import org.apache.commons.lang3.StringUtils;
import org.apache.hertzbeat.alert.dao.SingleAlertDao;
import org.apache.hertzbeat.alert.util.AlertUtil;
import org.apache.hertzbeat.common.constants.CommonConstants;
@@ -24,7 +27,6 @@ import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
@@ -33,49 +35,78 @@ import java.util.concurrent.ConcurrentHashMap;
@Component
public class AlarmCacheManager {
private static final String CUSTOM_FIRING_ROW_KEY = "CUSTOM_FIRING_";
/**
* The alarm in the process is triggered
* key - labels fingerprint
* rowKey - define id
* columnKey - labels fingerprint
*/
private final Map<String, SingleAlert> pendingAlertMap;
private final Table<String, String, SingleAlert> pendingAlertMap;
/**
* The not recover alert
* key - labels fingerprint
* rowKey - define id
* columnKey - labels fingerprint
*/
private final Map<String, SingleAlert> firingAlertMap;
private final Table<String, String, SingleAlert> firingAlertMap;
public AlarmCacheManager(SingleAlertDao singleAlertDao) {
this.pendingAlertMap = new ConcurrentHashMap<>(8);
this.firingAlertMap = new ConcurrentHashMap<>(8);
this.pendingAlertMap = Tables.newCustomTable(new ConcurrentHashMap<>(8), ConcurrentHashMap::new);
this.firingAlertMap = Tables.newCustomTable(new ConcurrentHashMap<>(8), ConcurrentHashMap::new);
List<SingleAlert> singleAlerts = singleAlertDao.querySingleAlertsByStatus(CommonConstants.ALERT_STATUS_FIRING);
for (SingleAlert singleAlert : singleAlerts) {
String fingerprint = AlertUtil.calculateFingerprint(singleAlert.getLabels());
String defineId = singleAlert.getLabels().get(CommonConstants.LABEL_DEFINE_ID);
if (StringUtils.isBlank(defineId)) {
defineId = getCustomKey(fingerprint);
}
singleAlert.setId(null);
this.firingAlertMap.put(fingerprint, singleAlert);
this.firingAlertMap.put(defineId, fingerprint, singleAlert);
}
}
public void putPending(String fingerPrint, SingleAlert alert) {
this.pendingAlertMap.put(fingerPrint, alert);
public void putPending(Long defineId, String fingerPrint, SingleAlert alert) {
this.pendingAlertMap.put(String.valueOf(defineId), fingerPrint, alert);
}
public SingleAlert getPending(String fingerPrint) {
return this.pendingAlertMap.get(fingerPrint);
public SingleAlert getPending(Long defineId, String fingerPrint) {
return this.pendingAlertMap.get(String.valueOf(defineId), fingerPrint);
}
public SingleAlert removePending(String fingerPrint) {
return this.pendingAlertMap.remove(fingerPrint);
public void removePending(Long defineId, String fingerPrint) {
this.pendingAlertMap.remove(String.valueOf(defineId), fingerPrint);
}
public void putFiring(Long defineId, String fingerPrint, SingleAlert alert) {
this.firingAlertMap.put(String.valueOf(defineId), fingerPrint, alert);
}
public void putFiring(String fingerPrint, SingleAlert alert) {
this.firingAlertMap.put(fingerPrint, alert);
this.firingAlertMap.put(getCustomKey(fingerPrint), fingerPrint, alert);
}
public SingleAlert getFiring(Long defineId, String fingerPrint) {
SingleAlert singleAlert = this.firingAlertMap.get(String.valueOf(defineId), fingerPrint);
if (null != singleAlert) {
return singleAlert;
}
return getFiring(fingerPrint);
}
public SingleAlert removeFiring(Long defineId, String fingerPrint) {
SingleAlert singleAlert = this.firingAlertMap.remove(String.valueOf(defineId), fingerPrint);
if (null == singleAlert) {
return this.firingAlertMap.remove(getCustomKey(fingerPrint), fingerPrint);
}
return singleAlert;
}
public SingleAlert getFiring(String fingerPrint) {
return this.firingAlertMap.get(fingerPrint);
return this.firingAlertMap.get(getCustomKey(fingerPrint), fingerPrint);
}
public SingleAlert removeFiring(String fingerPrint) {
return this.firingAlertMap.remove(fingerPrint);
private String getCustomKey(String fingerPrint) {
return CUSTOM_FIRING_ROW_KEY + fingerPrint;
}
}
@@ -17,8 +17,9 @@
package org.apache.hertzbeat.alert.calculate;
import java.util.HashMap;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
import org.apache.hertzbeat.alert.service.DataSourceService;
import org.apache.hertzbeat.alert.util.AlertTemplateUtil;
@@ -26,11 +27,11 @@ import org.apache.hertzbeat.alert.util.AlertUtil;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.stereotype.Component;
/**
* Periodic Alert Calculator
@@ -54,9 +55,9 @@ public class PeriodicAlertCalculator {
this.alarmCacheManager = alarmCacheManager;
}
public void calculate(AlertDefine rule) {
if (!rule.isEnable() || StringUtils.isEmpty(rule.getExpr())) {
log.error("Periodic rule {} is disabled or expression is empty", rule.getName());
public void calculate(AlertDefine define) {
if (!define.isEnable() || StringUtils.isEmpty(define.getExpr())) {
log.error("Periodic define {} is disabled or expression is empty", define.getName());
return;
}
long currentTimeMilli = System.currentTimeMillis();
@@ -66,8 +67,8 @@ public class PeriodicAlertCalculator {
// the return result should be matched with threshold
try {
List<Map<String, Object>> results = dataSourceService.calculate(
rule.getDatasource(),
rule.getExpr()
define.getDatasource(),
define.getExpr()
);
// if no match the expr threshold, the results item map {'value': null} should be null and others field keep
// if results has multi list, should trigger multi alert
@@ -77,8 +78,9 @@ public class PeriodicAlertCalculator {
for (Map<String, Object> result : results) {
Map<String, String> fingerPrints = new HashMap<>(8);
// here use the alert name as finger, not care the alert name may be changed
fingerPrints.put(CommonConstants.LABEL_ALERT_NAME, rule.getName());
fingerPrints.putAll(rule.getLabels());
fingerPrints.put(CommonConstants.LABEL_DEFINE_ID, String.valueOf(define.getId()));
fingerPrints.put(CommonConstants.LABEL_ALERT_NAME, define.getName());
fingerPrints.putAll(define.getLabels());
for (Map.Entry<String, Object> entry : result.entrySet()) {
if (entry.getValue() != null && !VALUE.equals(entry.getKey())
&& !TIMESTAMP.equals(entry.getKey())) {
@@ -87,32 +89,33 @@ public class PeriodicAlertCalculator {
}
if (result.get(VALUE) == null) {
// recovery the alert
handleRecoveredAlert(fingerPrints);
handleRecoveredAlert(define.getId(), fingerPrints);
continue;
}
Map<String, Object> fieldValueMap = new HashMap<>(8);
fieldValueMap.putAll(rule.getLabels());
fieldValueMap.put(CommonConstants.LABEL_ALERT_NAME, rule.getName());
fieldValueMap.putAll(define.getLabels());
fieldValueMap.put(CommonConstants.LABEL_ALERT_NAME, define.getName());
for (Map.Entry<String, Object> entry : result.entrySet()) {
if (entry.getValue() != null) {
fieldValueMap.put(entry.getKey(), entry.getValue());
}
}
afterThresholdRuleMatch(currentTimeMilli, fingerPrints, fieldValueMap, rule);
afterThresholdRuleMatch(currentTimeMilli, fingerPrints, fieldValueMap, define);
}
} catch (Exception ignored) {
// ignore the query exception eg: no result, timeout, etc
return;
}
} catch (Exception e) {
log.error("Calculate periodic rule {} failed: {}", rule.getName(), e.getMessage());
log.error("Calculate periodic define {} failed: {}", define.getName(), e.getMessage());
}
}
private void afterThresholdRuleMatch(long currentTimeMilli, Map<String, String> fingerPrints,
Map<String, Object> fieldValueMap, AlertDefine define) {
Long defineId = define.getId();
String fingerprint = AlertUtil.calculateFingerprint(fingerPrints);
SingleAlert existingAlert = alarmCacheManager.getPending(fingerprint);
SingleAlert existingAlert = alarmCacheManager.getPending(defineId, fingerprint);
Map<String, String> labels = new HashMap<>(8);
fieldValueMap.putAll(define.getLabels());
labels.putAll(fingerPrints);
@@ -133,11 +136,11 @@ public class PeriodicAlertCalculator {
// If required trigger times is 1, set to firing status directly
if (requiredTimes <= 1) {
newAlert.setStatus(CommonConstants.ALERT_STATUS_FIRING);
alarmCacheManager.putFiring(fingerprint, newAlert);
alarmCacheManager.putFiring(defineId, fingerprint, newAlert);
alarmCommonReduce.reduceAndSendAlarm(newAlert.clone());
} else {
// Otherwise put into pending queue first
alarmCacheManager.putPending(fingerprint, newAlert);
alarmCacheManager.putPending(defineId, fingerprint, newAlert);
}
} else {
// Update existing alert
@@ -147,17 +150,17 @@ public class PeriodicAlertCalculator {
// Check if required trigger times reached
if (existingAlert.getStatus().equals(CommonConstants.ALERT_STATUS_PENDING) && existingAlert.getTriggerTimes() >= requiredTimes) {
// Reached trigger times threshold, change to firing status
alarmCacheManager.removePending(fingerprint);
alarmCacheManager.removePending(defineId, fingerprint);
existingAlert.setStatus(CommonConstants.ALERT_STATUS_FIRING);
alarmCacheManager.putFiring(fingerprint, existingAlert);
alarmCacheManager.putFiring(defineId, fingerprint, existingAlert);
alarmCommonReduce.reduceAndSendAlarm(existingAlert.clone());
}
}
}
private void handleRecoveredAlert(Map<String, String> fingerprints) {
private void handleRecoveredAlert(Long defineId, Map<String, String> fingerprints) {
String fingerprint = AlertUtil.calculateFingerprint(fingerprints);
SingleAlert firingAlert = alarmCacheManager.removeFiring(fingerprint);
SingleAlert firingAlert = alarmCacheManager.removeFiring(defineId, fingerprint);
if (firingAlert != null) {
// todo consider multi times to tig for resolved alert
firingAlert.setTriggerTimes(1);
@@ -165,7 +168,7 @@ public class PeriodicAlertCalculator {
firingAlert.setStatus(CommonConstants.ALERT_STATUS_RESOLVED);
alarmCommonReduce.reduceAndSendAlarm(firingAlert.clone());
}
alarmCacheManager.removePending(fingerprint);
alarmCacheManager.removePending(defineId, fingerprint);
}
}
@@ -183,9 +183,11 @@ public class RealTimeAlertCalculator {
if (StringUtils.isBlank(expr)) {
continue;
}
Long defineId = define.getId();
Map<String, String> commonFingerPrints = new HashMap<>(8);
commonFingerPrints.put(CommonConstants.LABEL_INSTANCE, instance);
// here use the alert name as finger, not care the alert name may be changed
commonFingerPrints.put(CommonConstants.LABEL_DEFINE_ID, String.valueOf(define.getId()));
commonFingerPrints.put(CommonConstants.LABEL_ALERT_NAME, define.getName());
commonFingerPrints.put(CommonConstants.LABEL_INSTANCE_NAME, instanceName);
commonFingerPrints.put(CommonConstants.LABEL_INSTANCE_HOST, instanceHost);
@@ -200,9 +202,9 @@ public class RealTimeAlertCalculator {
try {
if (match) {
// If the threshold rule matches, the number of times the threshold has been triggered is determined and an alarm is triggered
afterThresholdRuleMatch(currentTimeMilli, commonFingerPrints, fieldValueMap, define, annotations);
afterThresholdRuleMatch(defineId, currentTimeMilli, commonFingerPrints, fieldValueMap, define, annotations);
} else {
handleRecoveredAlert(commonFingerPrints);
handleRecoveredAlert(defineId, commonFingerPrints);
}
// if this threshold pre compile success, ignore blew
continue;
@@ -254,9 +256,9 @@ public class RealTimeAlertCalculator {
boolean match = execAlertExpression(fieldValueMap, expr, false);
try {
if (match) {
afterThresholdRuleMatch(currentTimeMilli, fingerPrints, fieldValueMap, define, annotations);
afterThresholdRuleMatch(defineId, currentTimeMilli, fingerPrints, fieldValueMap, define, annotations);
} else {
handleRecoveredAlert(fingerPrints);
handleRecoveredAlert(defineId, fingerPrints);
}
} catch (Exception e) {
log.error(e.getMessage(), e);
@@ -334,9 +336,9 @@ public class RealTimeAlertCalculator {
.collect(Collectors.toList());
}
private void handleRecoveredAlert(Map<String, String> fingerprints) {
private void handleRecoveredAlert(Long defineId, Map<String, String> fingerprints) {
String fingerprint = AlertUtil.calculateFingerprint(fingerprints);
SingleAlert firingAlert = alarmCacheManager.removeFiring(fingerprint);
SingleAlert firingAlert = alarmCacheManager.removeFiring(defineId, fingerprint);
if (firingAlert != null) {
// todo consider multi times to tig for resolved alert
firingAlert.setTriggerTimes(1);
@@ -344,13 +346,14 @@ public class RealTimeAlertCalculator {
firingAlert.setStatus(CommonConstants.ALERT_STATUS_RESOLVED);
alarmCommonReduce.reduceAndSendAlarm(firingAlert.clone());
}
alarmCacheManager.removePending(fingerprint);
alarmCacheManager.removePending(defineId, fingerprint);
}
private void afterThresholdRuleMatch(long currentTimeMilli, Map<String, String> fingerPrints,
Map<String, Object> fieldValueMap, AlertDefine define, Map<String, String> annotations) {
private void afterThresholdRuleMatch(long defineId, long currentTimeMilli, Map<String, String> fingerPrints,
Map<String, Object> fieldValueMap, AlertDefine define,
Map<String, String> annotations) {
String fingerprint = AlertUtil.calculateFingerprint(fingerPrints);
SingleAlert existingAlert = alarmCacheManager.getPending(fingerprint);
SingleAlert existingAlert = alarmCacheManager.getPending(defineId, fingerprint);
fieldValueMap.putAll(define.getLabels());
int requiredTimes = define.getTimes() == null ? 1 : define.getTimes();
if (existingAlert == null) {
@@ -382,11 +385,11 @@ public class RealTimeAlertCalculator {
// If required trigger times is 1, set to firing status directly
if (requiredTimes <= 1) {
newAlert.setStatus(CommonConstants.ALERT_STATUS_FIRING);
alarmCacheManager.putFiring(fingerprint, newAlert);
alarmCacheManager.putFiring(defineId, fingerprint, newAlert);
alarmCommonReduce.reduceAndSendAlarm(newAlert.clone());
} else {
// Otherwise put into pending queue first
alarmCacheManager.putPending(fingerprint, newAlert);
alarmCacheManager.putPending(define.getId(), fingerprint, newAlert);
}
} else {
// Update existing alert
@@ -396,9 +399,9 @@ public class RealTimeAlertCalculator {
// Check if required trigger times reached
if (existingAlert.getStatus().equals(CommonConstants.ALERT_STATUS_PENDING) && existingAlert.getTriggerTimes() >= requiredTimes) {
// Reached trigger times threshold, change to firing status
alarmCacheManager.removePending(fingerprint);
alarmCacheManager.removePending(defineId, fingerprint);
existingAlert.setStatus(CommonConstants.ALERT_STATUS_FIRING);
alarmCacheManager.putFiring(fingerprint, existingAlert);
alarmCacheManager.putFiring(defineId, fingerprint, existingAlert);
alarmCommonReduce.reduceAndSendAlarm(existingAlert.clone());
}
}
@@ -28,6 +28,9 @@ import org.apache.hertzbeat.alert.dto.ExportAlertDefineDTO;
import org.apache.hertzbeat.alert.service.AlertDefineImExportService;
import org.apache.hertzbeat.alert.service.AlertDefineService;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.apache.hertzbeat.common.util.LogUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.context.annotation.Lazy;
import org.springframework.util.CollectionUtils;
@@ -41,12 +44,15 @@ public abstract class AlertDefineAbstractImExportServiceImpl implements AlertDef
@Lazy
private AlertDefineService alertDefineService;
private static final Logger logger = LoggerFactory.getLogger(AlertDefineAbstractImExportServiceImpl.class);
@Override
public void importConfig(InputStream is) {
var formList = parseImport(is)
.stream()
.map(this::convert)
.toList();
LogUtil.info(logger, "Importing alert defines from {0}", formList);
if (!CollectionUtils.isEmpty(formList)) {
formList.forEach(alertDefine -> {
alertDefineService.validate(alertDefine, false);
@@ -27,11 +27,14 @@ import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate;
import org.apache.hertzbeat.common.support.exception.SendMessageException;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.apache.hertzbeat.common.util.LogUtil;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
@@ -62,6 +65,7 @@ public class AlibabaSmsClientImpl implements SmsClient {
private final String accessKeySecret;
private final String signName;
private final String templateCode;
private static final Logger logger = LoggerFactory.getLogger(AlibabaSmsClientImpl.class);
public AlibabaSmsClientImpl(AlibabaSmsProperties config) {
if (config != null) {
@@ -173,7 +177,7 @@ public class AlibabaSmsClientImpl implements SmsClient {
log.info("Successfully sent SMS to phone: {}", phoneNumber);
}
} catch (Exception e) {
log.warn("Failed to send SMS: {}", e.getMessage());
LogUtil.warn(logger, "Failed to send SMS: {0}", e.getMessage());
throw new SendMessageException(e.getMessage());
}
}
@@ -192,6 +196,7 @@ public class AlibabaSmsClientImpl implements SmsClient {
// Step 4: Build authorization header
return ALGORITHM + " Credential=" + accessKeyId + ",SignedHeaders=host;x-acs-action;x-acs-content-sha256;x-acs-date;" + "x-acs-signature-nonce;x-acs-version,Signature=" + signature;
} catch (Exception e) {
LogUtil.warn(logger, "Failed to calculate authorization {0}", e.getMessage());
throw new RuntimeException("Failed to calculate authorization", e);
}
}
@@ -0,0 +1,135 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.calculate;
import org.apache.hertzbeat.alert.dao.SingleAlertDao;
import org.apache.hertzbeat.alert.util.AlertUtil;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.Mockito.when;
/**
* alert cache manager test
*/
@ExtendWith(MockitoExtension.class)
public class AlarmCacheManagerTest {
@Mock
private SingleAlertDao singleAlertDao;
private AlarmCacheManager alarmCacheManager;
@BeforeEach
public void setUp() {
Map<String, String> labels = new HashMap<>();
labels.put(CommonConstants.LABEL_ALERT_SEVERITY, CommonConstants.ALERT_SEVERITY_CRITICAL);
labels.put(CommonConstants.LABEL_DEFINE_ID, String.valueOf(1L));
SingleAlert alert = new SingleAlert();
alert.setContent("Alert cache manager test");
alert.setLabels(labels);
when(singleAlertDao.querySingleAlertsByStatus(CommonConstants.ALERT_STATUS_FIRING)).thenReturn(Collections.singletonList(alert));
alarmCacheManager = new AlarmCacheManager(singleAlertDao);
}
@Test
void testInit() {
Map<String, String> labels = new HashMap<>();
labels.put(CommonConstants.LABEL_ALERT_SEVERITY, CommonConstants.ALERT_SEVERITY_CRITICAL);
labels.put(CommonConstants.LABEL_DEFINE_ID, String.valueOf(1L));
String fingerprint = AlertUtil.calculateFingerprint(labels);
SingleAlert firingSingleAlert = alarmCacheManager.getFiring(1L, fingerprint);
assertNotNull(firingSingleAlert);
assertEquals("Alert cache manager test", firingSingleAlert.getContent());
alarmCacheManager.removeFiring(1L, fingerprint);
firingSingleAlert = alarmCacheManager.getFiring(1L, fingerprint);
assertNull(firingSingleAlert);
}
@Test
void testPending() {
Map<String, String> labels = new HashMap<>();
labels.put(CommonConstants.LABEL_ALERT_SEVERITY, CommonConstants.ALERT_SEVERITY_CRITICAL);
labels.put(CommonConstants.ALERT_SEVERITY_INFO, CommonConstants.ALERT_STATUS_PENDING);
labels.put(CommonConstants.LABEL_DEFINE_ID, String.valueOf(2L));
SingleAlert alert = new SingleAlert();
alert.setContent("Alert cache manager test");
alert.setLabels(labels);
String fingerprint = AlertUtil.calculateFingerprint(alert.getLabels());
alarmCacheManager.putPending(2L, fingerprint, alert);
SingleAlert pendingSingleAlert = alarmCacheManager.getPending(2L, fingerprint);
assertNotNull(pendingSingleAlert);
alarmCacheManager.removePending(2L, fingerprint);
pendingSingleAlert = alarmCacheManager.getPending(2L, fingerprint);
assertNull(pendingSingleAlert);
}
@Test
void testFiring() {
Map<String, String> labels = new HashMap<>();
labels.put(CommonConstants.LABEL_ALERT_SEVERITY, CommonConstants.ALERT_SEVERITY_CRITICAL);
labels.put(CommonConstants.ALERT_SEVERITY_INFO, CommonConstants.ALERT_STATUS_PENDING);
labels.put(CommonConstants.LABEL_DEFINE_ID, String.valueOf(3L));
SingleAlert alert = new SingleAlert();
alert.setContent("Alert cache manager test");
alert.setLabels(labels);
String fingerprint = AlertUtil.calculateFingerprint(alert.getLabels());
alarmCacheManager.putFiring(3L, fingerprint, alert);
SingleAlert firingSingleAlert = alarmCacheManager.getFiring(3L, fingerprint);
assertNotNull(firingSingleAlert);
alarmCacheManager.removeFiring(3L, fingerprint);
firingSingleAlert = alarmCacheManager.getFiring(3L, fingerprint);
assertNull(firingSingleAlert);
}
@Test
void testHistorical() {
SingleAlert alert = new SingleAlert();
alert.setContent("Alert cache manager test");
alert.setLabels(Collections.singletonMap(CommonConstants.LABEL_ALERT_SEVERITY, CommonConstants.ALERT_SEVERITY_CRITICAL));
when(singleAlertDao.querySingleAlertsByStatus(CommonConstants.ALERT_STATUS_FIRING)).thenReturn(Collections.singletonList(alert));
alarmCacheManager = new AlarmCacheManager(singleAlertDao);
String fingerprint = AlertUtil.calculateFingerprint(alert.getLabels());
SingleAlert historicalSingleAlert = alarmCacheManager.getFiring(4L, fingerprint);
assertNotNull(historicalSingleAlert);
SingleAlert singleAlert = alarmCacheManager.removeFiring(4L, fingerprint);
assertNotNull(singleAlert);
historicalSingleAlert = alarmCacheManager.getFiring(4L, fingerprint);
assertNull(historicalSingleAlert);
}
}
@@ -42,6 +42,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -92,12 +93,12 @@ class PeriodicAlertCalculatorTest {
result.put("__value__", 95.0); // Non-null, matched with threshold
result.put("__timestamp__", System.currentTimeMillis());
when(dataSourceService.calculate(anyString(), anyString())).thenReturn(List.of(result));
when(alarmCacheManager.getPending(anyString())).thenReturn(null);
when(alarmCacheManager.getPending(eq(rule.getId()), anyString())).thenReturn(null);
periodicAlertCalculator.calculate(rule);
// Verify that putFiring is called
ArgumentCaptor<String> idCaptor = ArgumentCaptor.forClass(String.class);
ArgumentCaptor<SingleAlert> alertCaptor = ArgumentCaptor.forClass(SingleAlert.class);
verify(alarmCacheManager).putFiring(idCaptor.capture(), alertCaptor.capture());
verify(alarmCacheManager).putFiring(eq(rule.getId()), idCaptor.capture(), alertCaptor.capture());
// Assertion alarm status and content
SingleAlert alert = alertCaptor.getValue();
assertAll(() -> assertEquals(CommonConstants.ALERT_STATUS_FIRING, alert.getStatus()),
@@ -112,7 +113,7 @@ class PeriodicAlertCalculatorTest {
result.put("__timestamp__", System.currentTimeMillis());
when(dataSourceService.calculate(anyString(), anyString())).thenReturn(List.of(result));
periodicAlertCalculator.calculate(rule);
verify(alarmCacheManager, times(0)).putFiring(any(), any());
verify(alarmCacheManager, times(0)).putFiring(any(), any(), any());
}
@Test
@@ -126,7 +127,7 @@ class PeriodicAlertCalculatorTest {
.triggerTimes(2).startAt(System.currentTimeMillis() - 60000)
.activeAt(System.currentTimeMillis() - 30000)
.build();
when(alarmCacheManager.removeFiring(anyString())).thenReturn(pendingAlert);
when(alarmCacheManager.removeFiring(eq(rule.getId()), anyString())).thenReturn(pendingAlert);
when(dataSourceService.calculate(anyString(), anyString())).thenReturn(List.of(result));
periodicAlertCalculator.calculate(rule);
ArgumentCaptor<SingleAlert> resolvedCaptor = ArgumentCaptor.forClass(SingleAlert.class);
@@ -132,6 +132,7 @@ public class RealTimeAlertCalculatorMatchTest {
AlertDefine matchDefine = new AlertDefine();
matchDefine.setId(1L);
matchDefine.setName("test");
matchDefine.setExpr(
"equals(__app__,\"prometheus\") && "
@@ -151,8 +152,8 @@ public class RealTimeAlertCalculatorMatchTest {
Thread.sleep(3000);
verify(alarmCacheManager, times(1)).getPending(any());
verify(alarmCacheManager, times(1)).putFiring(any(), any());
verify(alarmCacheManager, times(1)).getPending(any(), any());
verify(alarmCacheManager, times(1)).putFiring(any(), any(), any());
verify(alarmCommonReduce, times(1)).reduceAndSendAlarm(any());
}
@@ -180,6 +181,7 @@ public class RealTimeAlertCalculatorMatchTest {
CollectRep.MetricsData metricsData = builder.build();
AlertDefine matchDefine = new AlertDefine();
matchDefine.setId(1L);
matchDefine.setName("test");
matchDefine.setExpr("equals(__app__,\"prometheus\") && equals(__metrics__,\"canal_instance\") && metric_value > 0");
matchDefine.setTemplate("Canal instance val: ${value}%");
@@ -194,8 +196,8 @@ public class RealTimeAlertCalculatorMatchTest {
Thread.sleep(3000);
verify(alarmCacheManager, times(1)).getPending(any());
verify(alarmCacheManager, times(1)).putFiring(any(), any());
verify(alarmCacheManager, times(1)).getPending(any(), any());
verify(alarmCacheManager, times(1)).putFiring(any(), any(), any());
verify(alarmCommonReduce, times(1)).reduceAndSendAlarm(any());
}
@@ -229,6 +231,7 @@ public class RealTimeAlertCalculatorMatchTest {
CollectRep.MetricsData metricsData = builder.build();
AlertDefine matchDefine = new AlertDefine();
matchDefine.setId(1L);
matchDefine.setName("test");
matchDefine.setExpr("equals(__app__,\"springboot3\") && equals(__metrics__,\"available\") && equals(__instance__, \"518679137103104\") && responseTime > 0");
matchDefine.setTemplate("Canal instance val: ${value}%");
@@ -243,8 +246,8 @@ public class RealTimeAlertCalculatorMatchTest {
Thread.sleep(3000);
verify(alarmCacheManager, times(1)).getPending(any());
verify(alarmCacheManager, times(1)).putFiring(any(), any());
verify(alarmCacheManager, times(1)).getPending(any(), any());
verify(alarmCacheManager, times(1)).putFiring(any(), any(), any());
verify(alarmCommonReduce, times(1)).reduceAndSendAlarm(any());
}
@@ -42,7 +42,6 @@ import javax.management.remote.JMXServiceURL;
import javax.management.remote.rmi.RMIConnectorServer;
import javax.naming.Context;
import javax.rmi.ssl.SslRMIClientSocketFactory;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.collector.collect.AbstractCollect;
import org.apache.hertzbeat.collector.collect.common.cache.AbstractConnection;
import org.apache.hertzbeat.collector.collect.common.cache.CacheIdentifier;
@@ -54,13 +53,15 @@ import org.apache.hertzbeat.common.entity.job.Metrics;
import org.apache.hertzbeat.common.entity.job.protocol.JmxProtocol;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.util.CommonUtil;
import org.apache.hertzbeat.common.util.LogUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* jmx protocol acquisition implementation
*/
@Slf4j
public class JmxCollectImpl extends AbstractCollect {
private static final String JMX_URL_PREFIX = "service:jmx:rmi:///jndi/rmi://";
@@ -75,6 +76,8 @@ public class JmxCollectImpl extends AbstractCollect {
private final ClassLoader jmxClassLoader;
private static final Logger logger = LoggerFactory.getLogger(JmxCollectImpl.class);
public JmxCollectImpl() {
jmxClassLoader = new JmxClassLoader(ClassLoader.getSystemClassLoader());
}
@@ -195,12 +198,12 @@ public class JmxCollectImpl extends AbstractCollect {
}
} catch (IOException exception) {
String errorMsg = CommonUtil.getMessageFromThrowable(exception);
log.error("JMX IOException :{}", errorMsg);
LogUtil.error(logger, "JMX IOException: {0}", errorMsg);
builder.setCode(CollectRep.Code.UN_CONNECTABLE);
builder.setMsg(errorMsg);
} catch (Exception e) {
String errorMsg = CommonUtil.getMessageFromThrowable(e);
log.error("JMX Error :{}", errorMsg);
LogUtil.error(logger, "JMX Error: {0}", errorMsg);
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg(errorMsg);
} finally {
@@ -221,7 +224,7 @@ public class JmxCollectImpl extends AbstractCollect {
for (Attribute attribute : attributeList.asList()) {
Object value = attribute.getValue();
if (value == null) {
log.info("attribute {} value is null.", attribute.getName());
LogUtil.info(logger, "attribute {0} value is null.", attribute.getName());
continue;
}
if (value instanceof Number || value instanceof String || value instanceof ObjectName
@@ -245,7 +248,7 @@ public class JmxCollectImpl extends AbstractCollect {
}
attributeValueMap.put(attribute.getName(), builder.toString());
} else {
log.warn("attribute value type {} not support.", value.getClass().getName());
LogUtil.warn(logger, "attribute value type {0} not support.", value.getClass().getName());
}
}
return attributeValueMap;
@@ -319,7 +322,7 @@ public class JmxCollectImpl extends AbstractCollect {
connectionCommonCache.addCache(identifier, new JmxConnect(conn));
return conn;
} catch (Exception e) {
log.error("Failed to connect to JMX server: {}", e.getMessage());
LogUtil.error(logger, "Failed to connect to JMX connection: {0}", e.getMessage());
throw new IOException("Failed to connect to JMX server: " + e.getMessage(), e);
}
}
@@ -87,6 +87,11 @@ public interface CommonConstants {
*/
String LABEL_INSTANCE = "instance";
/**
* label key: defineid
*/
String LABEL_DEFINE_ID = "defineid";
/**
* label key: alert name
*/
@@ -0,0 +1,167 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.common.util;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.slf4j.Logger;
import java.text.MessageFormat;
/**
* Log utility class that provides formatted logging methods with location information.
* This class enhances standard SLF4J logging by automatically adding caller location details.
*/
public class LogUtil {
private static final String TEMPLATE_REGEX = "\\{\\d}";
/**
* Print debug level formatted log
* Example: LogUtil.debug(logger, "hello,{0},here has a {1} exception", "other information");
*/
@SuppressWarnings("unused")
public static void debug(Logger logger, String msg, Object... params) {
if (logger.isDebugEnabled()) {
if (ArrayUtils.isEmpty(params)) {
logger.debug(LogUtil.buildLocationInfo() + msg);
} else {
logger.debug(LogUtil.buildLocationInfo() + format(msg, params));
}
}
}
/**
* Print info level formatted log
* Example: LogUtil.info(logger, "hello,{0},{1} exception", "dear", "database operation");
*/
public static void info(Logger logger, String msg, Object... params) {
if (logger.isInfoEnabled()) {
if (ArrayUtils.isEmpty(params)) {
logger.info(LogUtil.buildLocationInfo() + msg);
} else {
logger.info(LogUtil.buildLocationInfo() + format(msg, params));
}
}
}
/**
* Print warn level formatted log
*/
public static void warn(Logger logger, String msg, Object... params) {
if (logger.isWarnEnabled()) {
if (ArrayUtils.isEmpty(params)) {
logger.warn(LogUtil.buildLocationInfo() + msg);
} else {
logger.warn(LogUtil.buildLocationInfo() + format(msg, params));
}
}
}
/**
* Print error level formatted log, use {0},{1},.. for parameter replacement
* Example: LogUtil.error(logger, "hello,{0}, a {1} exception occurred here", "dear", "database operation");
*/
public static void error(Logger logger, String msg, Object... params) {
if (logger.isErrorEnabled()) {
if (ArrayUtils.isEmpty(params)) {
logger.error(LogUtil.buildLocationInfo() + msg);
} else {
logger.error(LogUtil.buildLocationInfo() + format(msg, params));
}
}
}
/**
* Print warn level formatted log with exception, use {0},{1},.. for parameter replacement
* Example: LogUtil.warn(logger, e, "hello,{0}, a {1} exception occurred here", "dear", "database operation");
*/
public static void warn(Logger logger, Throwable e, String msg, Object... params) {
if (logger.isWarnEnabled()) {
if (ArrayUtils.isEmpty(params)) {
logger.warn(LogUtil.buildLocationInfo() + msg, e);
} else {
logger.warn(LogUtil.buildLocationInfo() + format(msg, params), e);
}
}
}
/**
* Print error level formatted log with exception, use {0},{1},.. for parameter replacement
* Example: LogUtil.error(logger, e, "hello,{0}, a {1} exception occurred here", "dear", "database operation");
*/
public static void error(Logger logger, Throwable e, String msg, Object... params) {
if (logger.isErrorEnabled()) {
if (ArrayUtils.isEmpty(params)) {
logger.error(LogUtil.buildLocationInfo() + msg, e);
} else {
logger.error(LogUtil.buildLocationInfo() + format(msg, params), e);
}
}
}
/**
* Get the class name, method and line number that calls LogUtil
*
* @return location information string
*/
private static String buildLocationInfo() {
StringBuilder header = new StringBuilder();
// LOG4J2-1029 new Throwable().getStackTrace is faster than Thread.currentThread().getStackTrace().
final StackTraceElement[] stackTraceElements = new Throwable().getStackTrace();
for (int i = 0; i < stackTraceElements.length - 1; i++) {
StackTraceElement currentStackTrace = stackTraceElements[i];
StackTraceElement nextStackTrace = stackTraceElements[i + 1];
// If current stack trace is in LogUtil
// and next stack trace is not in LogUtil
// then the next node is the caller of LogUtil
if (LogUtil.class.getName().equals(currentStackTrace.getClassName())
&& !LogUtil.class.getName().equals(nextStackTrace.getClassName())) {
String stackTrace = nextStackTrace.toString();
header.append(" ").append(StringUtils.removeStart(stackTrace, nextStackTrace.getClassName() + "."));
break;
}
}
return header.append(":").toString();
}
private static String format(String msg, Object... params) {
if (StringUtils.isEmpty(msg)) {
return StringUtils.EMPTY;
}
if (params != null && params.length > 0) {
msg = MessageFormat.format(msg, params);
}
return msg.replaceAll(TEMPLATE_REGEX, StringUtils.EMPTY);
}
private static String toString(Object object) {
return ToStringBuilder.reflectionToString(object, ToStringStyle.SHORT_PREFIX_STYLE);
}
}
@@ -0,0 +1,117 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.common.util;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.slf4j.Logger;
import java.lang.reflect.Method;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.anyString;
import static org.mockito.Mockito.contains;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class LogUtilTest {
@Mock
private Logger mockLogger;
private AutoCloseable mocks;
@BeforeEach
void setUp() {
mocks = MockitoAnnotations.openMocks(this);
}
@AfterEach
void tearDown() throws Exception {
if (mocks != null) {
mocks.close();
}
}
@Test
void testFormat_noParams_returnsOriginalMessage() throws Exception {
String original = "hello world";
Method formatMethod = LogUtil.class.getDeclaredMethod("format", String.class, Object[].class);
formatMethod.setAccessible(true);
String formatted = (String) formatMethod.invoke(null, original, new Object[0]);
assertEquals(original, formatted);
}
@Test
void testFormat_withParams_replacesPlaceholders() throws Exception {
String template = "hello,{0}, world {1}!";
Method formatMethod = LogUtil.class.getDeclaredMethod("format", String.class, Object[].class);
formatMethod.setAccessible(true);
Object[] params = {"Alice", 123};
String result = (String) formatMethod.invoke(null, template, params);
assertTrue(result.contains("hello,Alice"));
assertTrue(result.contains("world 123!"));
}
@Test
void testDebug_noParams_logsRawMessage() {
when(mockLogger.isDebugEnabled()).thenReturn(true);
String msg = "test-debug";
LogUtil.debug(mockLogger, msg);
verify(mockLogger).debug(contains(msg));
}
@Test
void testDebug_withParams_logsFormattedMessage() {
when(mockLogger.isDebugEnabled()).thenReturn(true);
LogUtil.debug(mockLogger, "user={0}", "Bob");
verify(mockLogger).debug(contains("user=Bob"));
}
@Test
void testInfo_levelOff_doesNotLog() {
when(mockLogger.isInfoEnabled()).thenReturn(false);
LogUtil.info(mockLogger, "should-not-log");
verify(mockLogger, never()).info(anyString());
}
@Test
void testWarn_withException_logsMessageAndException() {
when(mockLogger.isWarnEnabled()).thenReturn(true);
RuntimeException ex = new RuntimeException("warn-ex");
LogUtil.warn(mockLogger, ex, "warning {0}", "occurred");
ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
verify(mockLogger).warn(captor.capture(), eq(ex));
assertTrue(captor.getValue().contains("warning occurred"));
}
@Test
void testError_withExceptionAndParams_logsError() {
when(mockLogger.isErrorEnabled()).thenReturn(true);
RuntimeException ex = new RuntimeException("err");
LogUtil.error(mockLogger, ex, "fail code {0}", 500);
ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
verify(mockLogger).error(captor.capture(), eq(ex));
assertTrue(captor.getValue().contains("fail code 500"));
}
}
@@ -121,6 +121,14 @@ public class MonitorsController {
monitorService.export(ids, type, res);
}
@GetMapping("/export/all")
@Operation(summary = "export all monitor config", description = "export all monitor config")
public void exportAll(
@Parameter(description = "Export Type:JSON,EXCEL,YAML") @RequestParam(defaultValue = "JSON") String type,
HttpServletResponse res) throws Exception {
monitorService.exportAll(type, res);
}
@PostMapping("/import")
@Operation(summary = "import monitor config", description = "import monitor config")
public ResponseEntity<Message<Void>> export(MultipartFile file) throws Exception {
@@ -173,6 +173,15 @@ public interface MonitorService {
*/
void export(List<Long> ids, String type, HttpServletResponse res) throws Exception;
/**
* Export All Monitoring Configuration
*
* @param type file type
* @param res response
* @throws Exception This exception will be thrown if the export fails
*/
void exportAll(String type, HttpServletResponse res) throws Exception;
/**
* Import Monitoring Configuration
*
@@ -235,6 +235,18 @@ public class MonitorServiceImpl implements MonitorService {
imExportService.exportConfig(res.getOutputStream(), ids);
}
@Override
public void exportAll(String type, HttpServletResponse res) throws Exception {
// Get all monitor IDs from the database
List<Long> allMonitorIds = monitorDao.findAll()
.stream()
.map(Monitor::getId)
.collect(Collectors.toList());
// Use the existing export method to export all monitors
export(allMonitorIds, type, res);
}
@Override
public void importConfig(MultipartFile file) throws Exception {
var fileName = FileUtil.getFileName(file);
@@ -18,11 +18,13 @@ app: kafka_client
name:
zh-CN: Kafka消息系统(客户端)
en-US: Kafka MessageClient
ja-JP: Kafkaメッセージングシステム(クライアント)
help:
zh-CN: HertzBeat 使用 <a href="https://hertzbeat.apache.org/zh-cn/docs/help/kafka_client">Kafka Admin Client</a> 对 Kafka 的通用指标进行采集监控。</span>
en-US: HertzBeat uses <a href='https://hertzbeat.apache.org/docs/help/kafka_client'>Kafka Admin Client</a> to monitoring kafka general metrics. </span>
zh-TW: HertzBeat 使用 <a href="https://hertzbeat.apache.org/zh-cn/docs/help/kafka_client">Kafka Admin Client</a> 對 Kafka 的通用指標進行采集監控。</span>
ja-JP: HertzBeat は <a href="https://hertzbeat.apache.org/docs/help/kafka_client">Kafka Admin Clientを介して</a> Kafkaの一般的なパフォーマンスのメトリクスを監視します。</span>
helpLink:
zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/kafka_client
@@ -33,12 +35,14 @@ params:
name:
zh-CN: 目标Host
en-US: Target Host
ja-JP: 目標ホスト
type: host
required: true
- field: port
name:
zh-CN: 端口
en-US: Port
ja-JP: ポート
type: number
range: '[0,65535]'
required: true
@@ -47,6 +51,7 @@ params:
name:
zh-CN: 是否监控内部主题
en-US: Monitor Internal Topic
ja-JP: 内部トピックを監視するかどうか
type: boolean
required: true
defaultValue: false
@@ -56,6 +61,7 @@ metrics:
i18n:
zh-CN: 主题列表
en-US: Topic List
ja-JP: トピック一覧
priority: 0
fields:
- field: TopicName
@@ -63,6 +69,7 @@ metrics:
i18n:
zh-CN: 主题名称
en-US: Topic Name
ja-JP: トピック名
protocol: kclient
kclient:
host: ^_^host^_^
@@ -73,6 +80,7 @@ metrics:
i18n:
zh-CN: 主题详细信息
en-US: Topic Detail Info
ja-JP: トピック詳細情報
priority: 1
fields:
- field: TopicName
@@ -80,36 +88,43 @@ metrics:
i18n:
zh-CN: 主题名称
en-US: Topic Name
ja-JP: トピック名
- field: PartitionNum
type: 1
i18n:
zh-CN: 分区数量
en-US: Partition Num
ja-JP: パーティション数
- field: PartitionLeader
type: 1
i18n:
zh-CN: 分区领导者
en-US: Partition Leader
ja-JP: パーティションリーダー
- field: BrokerHost
type: 1
i18n:
zh-CN: Broker主机
en-US: Broker Host
ja-JP: ブローカーホスト
- field: BrokerPort
type: 1
i18n:
zh-CN: Broker端口
en-US: Broker Port
ja-JP: ブローカーポート
- field: ReplicationFactorSize
type: 1
i18n:
zh-CN: 复制因子大小
en-US: Replication Factor Size
ja-JP: レプリカファクターのサイズ
- field: ReplicationFactor
type: 1
i18n:
zh-CN: 复制因子
en-US: Replication Factor
ja-JP: レプリカファクター
protocol: kclient
kclient:
host: ^_^host^_^
@@ -120,6 +135,7 @@ metrics:
i18n:
zh-CN: 主题偏移量
en-US: Topic Offset
ja-JP: トピックオフセット
priority: 2
# Kafka offset does not need to be obtained frequently, as getting it too quickly will affect performance
interval: 300
@@ -130,22 +146,26 @@ metrics:
i18n:
zh-CN: 主题名称
en-US: Topic Name
ja-JP: トピック名
- field: PartitionNum
label: true
type: 1
i18n:
zh-CN: 分区号
en-US: Partition Num
ja-JP: パーティション数
- field: earliest
type: 0
i18n:
zh-CN: 最早偏移量
en-US: Earliest Offset
ja-JP: 最早オフセット
- field: latest
type: 0
i18n:
zh-CN: 最新偏移量
en-US: Latest Offset
ja-JP: 最新オフセット
protocol: kclient
kclient:
host: ^_^host^_^
@@ -156,6 +176,7 @@ metrics:
i18n:
zh-CN: 消费者组情况
en-US: Consumer Detail Info
ja-JP: 消費者グループ詳細情報
priority: 3
# Kafka offset does not need to be obtained frequently, as getting it too quickly will affect performance
interval: 300
@@ -166,27 +187,32 @@ metrics:
i18n:
zh-CN: 消费者组ID
en-US: Consumer Group ID
ja-JP: 消費者グループID
- field: Group Member Num
type: 1
i18n:
zh-CN: 消费者实例数量
en-US: Group Member Num
ja-JP: 消費者グループのメンバー数
- field: Topic
label: true
type: 1
i18n:
zh-CN: 订阅主题名称
en-US: Subscribed Topic Name
ja-JP: 購読されたトピック名
- field: Offset of Each Partition
type: 1
i18n:
zh-CN: 各分区偏移量
en-US: Offset of Each Partition
ja-JP: 各パーティションのオフセット
- field: Lag
type: 0
i18n:
zh-CN: 落后偏移量
en-US: Total Lag
ja-JP: ラグオフセット
protocol: kclient
kclient:
host: ^_^host^_^
@@ -145,4 +145,18 @@ class MonitorsControllerTest {
.andExpect(jsonPath("$.code").value("0"))
.andExpect(jsonPath("$.msg").value("Import success"));
}
@Test
void exportAll() throws Exception {
String type = "JSON";
// Mock the behavior of monitorService.exportAll
doNothing().when(monitorService).exportAll(Mockito.anyString(), Mockito.any());
// Perform the request and verify the response
this.mockMvc.perform(MockMvcRequestBuilders.get("/api/monitors/export/all")
.param("type", type))
.andExpect(status().isOk())
.andReturn();
}
}
@@ -745,6 +745,33 @@ class MonitorServiceTest {
when(monitorDao.findById(1L)).thenReturn(Optional.of(monitor));
when(paramDao.findParamsByMonitorId(1L)).thenReturn(params);
assertDoesNotThrow(() -> monitorService.copyMonitor(1L));
}
@Test
void exportAll() throws Exception {
// Create some test monitors
Monitor monitor1 = Monitor.builder().id(1L).name("test1").app("app1").build();
Monitor monitor2 = Monitor.builder().id(2L).name("test2").app("app2").build();
List<Monitor> allMonitors = List.of(monitor1, monitor2);
// Mock the behavior of monitorDao.findAll
when(monitorDao.findAll()).thenReturn(allMonitors);
// Create a mock HttpServletResponse
jakarta.servlet.http.HttpServletResponse mockResponse = org.mockito.Mockito.mock(jakarta.servlet.http.HttpServletResponse.class);
// Mock the ImExportService
org.apache.hertzbeat.manager.service.ImExportService mockImExportService = org.mockito.Mockito.mock(org.apache.hertzbeat.manager.service.ImExportService.class);
// Mock the getFileName method
when(mockImExportService.getFileName()).thenReturn("test.json");
// Set the field using reflection
java.lang.reflect.Field field = MonitorServiceImpl.class.getDeclaredField("imExportServiceMap");
field.setAccessible(true);
java.util.Map<String, org.apache.hertzbeat.manager.service.ImExportService> imExportServiceMap = new java.util.HashMap<>();
imExportServiceMap.put("JSON", mockImExportService);
field.set(monitorService, imExportServiceMap);
// Test the exportAll method
assertDoesNotThrow(() -> monitorService.exportAll("JSON", mockResponse));
}
}
@@ -1,4 +1,6 @@
<router-outlet></router-outlet>
<div class="alain-default__content">
<router-outlet></router-outlet>
</div>
<div class="ai-chatbot-container">
<div class="ai-chatbot-button" (click)="toggleChatbot()">
File diff suppressed because it is too large Load Diff
+20 -75
View File
@@ -13,107 +13,52 @@ import { AiBotService, ChatMessage } from '../../shared/services/ai-bot.service'
selector: 'layout-basic',
template: `
<layout-default [options]="options" [nav]="navTpl" [content]="contentTpl" [customError]="null">
<!-- 左侧菜单项 - GitHub链接 -->
<layout-default-header-item direction="left">
<a
layout-default-header-item-trigger
href="//github.com/apache/hertzbeat"
target="_blank"
class="modern-header-item github-link"
>
<div class="icon-wrapper">
<i nz-icon nzType="github" class="header-icon"></i>
</div>
<span class="item-tooltip">GitHub</span>
<a layout-default-header-item-trigger href="//github.com/apache/hertzbeat" target="_blank">
<i nz-icon nzType="github"></i>
</a>
</layout-default-header-item>
<!-- 移动端搜索按钮 -->
<layout-default-header-item direction="left" hidden="pc">
<div
layout-default-header-item-trigger
(click)="searchToggleStatus = !searchToggleStatus"
class="modern-header-item search-toggle"
>
<div class="icon-wrapper">
<i nz-icon nzType="search" class="header-icon"></i>
</div>
<div layout-default-header-item-trigger (click)="searchToggleStatus = !searchToggleStatus">
<i nz-icon nzType="search"></i>
</div>
</layout-default-header-item>
<!-- 中间搜索栏 -->
<layout-default-header-item direction="middle">
<header-search class="alain-default__search modern-search" [toggleChange]="searchToggleStatus"></header-search>
<header-search class="alain-default__search" [toggleChange]="searchToggleStatus"></header-search>
</layout-default-header-item>
<!-- 右侧通知 -->
<layout-default-header-item direction="right" hidden="mobile">
<header-notify class="modern-header-item notification-item">
</header-notify>
<header-notify></header-notify>
</layout-default-header-item>
<!-- 锁定按钮 -->
<layout-default-header-item direction="right" hidden="mobile">
<a
layout-default-header-item-trigger
routerLink="/passport/lock"
class="modern-header-item lock-item"
>
<div class="icon-wrapper">
<i nz-icon nzType="lock" class="header-icon"></i>
</div>
<span class="item-tooltip">{{ 'menu.lock' | i18n }}</span>
<a layout-default-header-item-trigger routerLink="/passport/lock">
<i nz-icon nzType="lock"></i>
</a>
</layout-default-header-item>
<!-- 设置下拉菜单 -->
<layout-default-header-item direction="right" hidden="mobile">
<div
layout-default-header-item-trigger
nz-dropdown
[nzDropdownMenu]="settingsMenu"
nzTrigger="click"
nzPlacement="bottomRight"
class="modern-header-item settings-item"
>
<div class="icon-wrapper">
<i nz-icon nzType="setting" class="header-icon spinning-on-hover"></i>
</div>
<span class="item-tooltip">{{ 'menu.settings' | i18n }}</span>
<div layout-default-header-item-trigger nz-dropdown [nzDropdownMenu]="settingsMenu" nzTrigger="click" nzPlacement="bottomRight">
<i nz-icon nzType="setting"></i>
</div>
<nz-dropdown-menu #settingsMenu="nzDropdownMenu">
<div nz-menu class="modern-dropdown-menu">
<div nz-menu-item class="modern-menu-item">
<div class="menu-item-content">
<i nz-icon nzType="fullscreen" class="menu-icon"></i>
<header-fullscreen></header-fullscreen>
</div>
<div nz-menu style="width: 200px;">
<div nz-menu-item>
<header-fullscreen></header-fullscreen>
</div>
<li nz-menu-divider class="modern-divider"></li>
<div nz-menu-item routerLink="/setting/labels" class="modern-menu-item">
<div class="menu-item-content">
<i nz-icon nzType="tag" class="menu-icon"></i>
<span class="menu-text">{{ 'menu.advanced.labels' | i18n }}</span>
</div>
<div nz-menu-item routerLink="/setting/labels">
<i nz-icon nzType="tag" class="mr-sm"></i>
<span style="margin-left: 4px">{{ 'menu.advanced.labels' | i18n }}</span>
</div>
<li nz-menu-divider class="modern-divider"></li>
<div nz-menu-item class="modern-menu-item">
<div class="menu-item-content">
<i nz-icon nzType="global" class="menu-icon"></i>
<header-i18n></header-i18n>
</div>
<div nz-menu-item>
<header-i18n></header-i18n>
</div>
</div>
</nz-dropdown-menu>
</layout-default-header-item>
<!-- 用户菜单 -->
<layout-default-header-item direction="right">
<header-user class="modern-header-item user-item">
</header-user>
<header-user></header-user>
</layout-default-header-item>
<ng-template #navTpl>
<layout-default-nav class="d-block py-lg modern-nav" openStrictly="true"></layout-default-nav>
<layout-default-nav class="d-block py-lg" openStrictly="true"></layout-default-nav>
</ng-template>
<ng-template #contentTpl>
<router-outlet></router-outlet>
@@ -77,6 +77,12 @@
{{ 'monitor.export' | i18n }}
</button>
</li>
<li nz-menu-item>
<button nz-button (click)="onExportAllMonitors()">
<i nz-icon nzType="export" nzTheme="outline"></i>
{{ 'monitor.export-all' | i18n }}
</button>
</li>
<li nz-menu-item>
<nz-upload nzAction="/monitors/import" [nzLimit]="1" [nzShowUploadList]="false" (nzChange)="onImportMonitors($event)">
<button nz-button>
@@ -287,7 +293,12 @@
>
<ng-container *nzModalContent>
<div class="export-type-container">
<div class="export-type-card" (click)="exportMonitors('JSON')" [class.loading]="exportJsonButtonLoading">
<div
class="export-type-card"
(click)="exportMonitors('JSON')"
[class.loading]="exportJsonButtonLoading"
*ngIf="checkedMonitorIds.size > 0"
>
<div class="export-type-icon">
<i nz-icon nzType="code" nzTheme="outline"></i>
</div>
@@ -296,7 +307,12 @@
<p>{{ 'monitor.export.use-type' | i18n : { type: 'JSON' } }}</p>
</div>
</div>
<div class="export-type-card" (click)="exportMonitors('EXCEL')" [class.loading]="exportExcelButtonLoading">
<div
class="export-type-card"
(click)="exportMonitors('EXCEL')"
[class.loading]="exportExcelButtonLoading"
*ngIf="checkedMonitorIds.size > 0"
>
<div class="export-type-icon">
<i nz-icon nzType="file-excel" nzTheme="outline"></i>
</div>
@@ -305,6 +321,34 @@
<p>{{ 'monitor.export.use-type' | i18n : { type: 'EXCEL' } }}</p>
</div>
</div>
<div
class="export-type-card"
(click)="exportAllMonitors('JSON')"
[class.loading]="exportJsonButtonLoading"
*ngIf="checkedMonitorIds.size === 0"
>
<div class="export-type-icon">
<i nz-icon nzType="code" nzTheme="outline"></i>
</div>
<div class="export-type-info">
<h3>JSON</h3>
<p>{{ 'monitor.export-all.use-type' | i18n : { type: 'JSON' } }}</p>
</div>
</div>
<div
class="export-type-card"
(click)="exportAllMonitors('EXCEL')"
[class.loading]="exportExcelButtonLoading"
*ngIf="checkedMonitorIds.size === 0"
>
<div class="export-type-icon">
<i nz-icon nzType="file-excel" nzTheme="outline"></i>
</div>
<div class="export-type-info">
<h3>EXCEL</h3>
<p>{{ 'monitor.export-all.use-type' | i18n : { type: 'EXCEL' } }}</p>
</div>
</div>
</div>
</ng-container>
</nz-modal>
@@ -264,6 +264,10 @@ export class MonitorListComponent implements OnInit, OnDestroy {
this.isSwitchExportTypeModalVisible = true;
}
onExportAllMonitors() {
this.isSwitchExportTypeModalVisible = true;
}
onImportMonitors(info: NzUploadChangeParam): void {
console.log(info.type);
if (info.type === 'start') {
@@ -362,6 +366,46 @@ export class MonitorListComponent implements OnInit, OnDestroy {
);
}
exportAllMonitors(type: string) {
switch (type) {
case 'JSON':
this.exportJsonButtonLoading = true;
break;
case 'EXCEL':
this.exportExcelButtonLoading = true;
break;
}
const exportAllMonitors$ = this.monitorSvc
.exportAllMonitors(type)
.pipe(
finalize(() => {
this.exportExcelButtonLoading = false;
this.exportJsonButtonLoading = false;
exportAllMonitors$.unsubscribe();
})
)
.subscribe(
response => {
const message = response.body!;
if (message.type == 'application/json') {
this.notifySvc.error(this.i18nSvc.fanyi('common.notify.export-fail'), '');
} else {
const blob = new Blob([message], { type: response.headers.get('Content-Type')! });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.download = response.headers.get('Content-Disposition')!.split(';')[1].split('filename=')[1];
a.href = url;
a.click();
window.URL.revokeObjectURL(url);
this.isSwitchExportTypeModalVisible = false;
}
},
error => {
this.notifySvc.error(this.i18nSvc.fanyi('common.notify.export-fail'), error.msg);
}
);
}
onCancelManageMonitors() {
if (this.checkedMonitorIds == null || this.checkedMonitorIds.size === 0) {
this.notifySvc.warning(this.i18nSvc.fanyi('common.notify.no-select-cancel'), '');
@@ -30,6 +30,7 @@ const monitors_uri = '/monitors';
const detect_monitor_uri = '/monitor/detect';
const manage_monitors_uri = '/monitors/manage';
const export_monitors_uri = '/monitors/export';
const export_all_monitors_uri = '/monitors/export/all';
const summary_uri = '/summary';
const warehouse_storage_status_uri = '/warehouse/storage/status';
const grafana_dashboard_uri = '/grafana/dashboard';
@@ -74,6 +75,16 @@ export class MonitorService {
});
}
public exportAllMonitors(type: string): Observable<HttpResponse<Blob>> {
let httpParams = new HttpParams();
httpParams = httpParams.append('type', type);
return this.http.get(export_all_monitors_uri, {
params: httpParams,
observe: 'response',
responseType: 'blob'
});
}
public cancelManageMonitors(monitorIds: Set<number>): Observable<Message<any>> {
let httpParams = new HttpParams();
monitorIds.forEach(monitorId => {
+10 -7
View File
@@ -70,11 +70,11 @@
"alert.help.inhibit.link": "https://hertzbeat.apache.org/docs/help/alert_inhibit",
"alert.help.integration": "Unified management of alerts from different third-party platforms, integrating and receiving alert messages from third-party monitoring and observability systems, and performing actions such as grouping, aggregation, inhibition, silencing, and notification distribution.",
"alert.help.integration.link": "https://hertzbeat.apache.org",
"alert.help.notice": "Notification is used to config the receiver of alarm message and receiving method. The alarm message will be sent to the receiver by specified way(support email, discord, webhook etc). <a href='https://hertzbeat.apache.org/zh-cn/docs/help/alert_webhook'>Click here to see configuration steps.</a>.<br><i>Notice Template</i> is message content structure template. The built-in template is used by default or you can customize the template to customize the message notification structure.<br><span class='help_module_span'>Note⚠️: After configuring the <i>Receiver</i>, you also need to config the<i>Notice Policy</i>to specify which messages are sent to which receivers.</span><a href='https://hertzbeat.apache.org/docs/help/alert_email'> Click here to see potential issues</a>.",
"alert.help.notice": "Notification is used to config the receiver of alarm message and receiving method. The alarm message will be sent to the receiver by specified way(support email, discord, webhook etc). <a href='https://hertzbeat.apache.org/zh-cn/docs/help/alert_webhook'>Click here to see configuration steps.</a>.<br>\"<i>Notice Template</i>\" is message content structure template. The built-in template is used by default or you can customize the template to customize the message notification structure.<br><span class='help_module_span'>Note⚠️: After configuring the \"<i>Receiver</i>\", you also need to config the\"<i>Notice Policy</i>\"to specify which messages are sent to which receivers.</span><a href='https://hertzbeat.apache.org/docs/help/alert_email'> Click here to see potential issues</a>.",
"alert.help.notice.link": "https://hertzbeat.apache.org/docs/help/alert_email",
"alert.help.setting": "Threshold Rules are used for metrics alarm threshold rule management. Click the \"<i>New Threshold</i>\" to configure the alarm threshold for monitoring metrics. Hertzbeat will trigger alarms based on the threshold and metrics data.<br>Note⚠️: The alarm message that has been triggered can be checked in [Alter Center], and you can also set the notification method and personnel in [Notification].",
"alert.help.setting.link": "https://hertzbeat.apache.org/docs/help/alert_threshold",
"alert.help.silence": "Alarm Silence management is used when you dont want to be disturbed during system maintenance or on nights weekend. <br> Click \"<i>New Silence Strategy</i>\" and configure the time period to block messages so you would not get disturbed during breaks.",
"alert.help.silence": "Alarm Silence management is used when you don't want to be disturbed during system maintenance or on nights weekend. <br> Click \"<i>New Silence Strategy</i>\" and configure the time period to block messages so you would not get disturbed during breaks.",
"alert.help.silence.link": "https://hertzbeat.apache.org/docs",
"alert.inhibit.delete": "Delete Inhibit Rule",
"alert.inhibit.edit": "Edit Inhibit Rule",
@@ -702,10 +702,12 @@
"monitor.edit-monitor": "Edit Monitor",
"monitor.edit.failed": "Update Monitor Failed",
"monitor.edit.success": "Update Monitor Success",
"monitor.enable": "Resume Monitor",
"monitor.export": "Export Monitor",
"monitor.export.switch-type": "Please select the export file format!",
"monitor.export.use-type": "Export monitors in {{type}} file format",
"monitor.enable": "Enable",
"monitor.export": "Export Selected",
"monitor.export-all": "Export All",
"monitor.export.switch-type": "Please select the export file format",
"monitor.export.use-type": "Export selected monitors in {{type}} format",
"monitor.export-all.use-type": "Export all monitors in {{type}} format",
"monitor.grafana.enabled.label": "Enable Grafana",
"monitor.grafana.enabled.tip": "is enabled, the monitoring data will be displayed in Grafana",
"monitor.grafana.upload.label": "Upload Grafana Template",
@@ -913,5 +915,6 @@
"ai.bot.greeting": "Hello! I am an AI assistant. How can I help you?",
"ai.bot.input.placeholder": "Please enter a question...",
"ai.bot.send": "Send",
"ai.bot.connect-fail": "Sorry, there was an issue connecting to the AI assistant. Please try again later."
"ai.bot.connect-fail": "Sorry, there was an issue connecting to the AI assistant. Please try again later.",
"monitor.help": "Monitoring and management page, you can check the metric data and manage monitoring tasks here. The status of normal service is"
}
+6 -3
View File
@@ -703,9 +703,11 @@
"monitor.edit.failed": "修改监控失败",
"monitor.edit.success": "修改监控成功",
"monitor.enable": "恢复监控",
"monitor.export": "导出监控",
"monitor.export": "导出所选",
"monitor.export-all": "导出全部",
"monitor.export.switch-type": "请选择导出文件格式!",
"monitor.export.use-type": "以 {{type}} 文件格式导出监控",
"monitor.export.use-type": "以 {{type}} 文件格式导出所选监控",
"monitor.export-all.use-type": "以 {{type}} 文件格式导出全部监控",
"monitor.grafana.enabled.label": "启用Grafana",
"monitor.grafana.enabled.tip": "是否启用Grafana",
"monitor.grafana.upload.label": "上传Grafana模板",
@@ -913,5 +915,6 @@
"ai.bot.greeting": "你好!我是AI助手,有什么可以帮助你的吗?",
"ai.bot.input.placeholder": "请输入问题...",
"ai.bot.send": "发送",
"ai.bot.connect-fail": "抱歉,连接AI助手时出现问题,请稍后再试。"
"ai.bot.connect-fail": "抱歉,连接AI助手时出现问题,请稍后再试。",
"monitor.help": "监控管理页面,您可以在此查看指标数据并管理监控任务。正常服务的状态为"
}