Compare commits

...
Author SHA1 Message Date
a-little-fool 5c98843efb correct home's new_committer_process 2025-02-21 17:52:39 +08:00
Nick Guo 398a40bba4 [improve] optimize kafka collect test (#3093) 2025-02-21 17:47:46 +08:00
yunfan24andtomsun28 bb14681621 [feature] supports alibabacloud sms. (#3084)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-02-21 13:55:30 +08:00
yunfan24 d3a080fbaf [improve] Improve and unify the search. (#3085) 2025-02-20 14:58:56 +08:00
tomsun28 ce6f9feac6 [webapp] update and fix alert ui when theme dark (#3082) 2025-02-16 23:25:04 +08:00
yunfan24 7d18118bbd [feature] SMS notification supports unisms. (#3077) 2025-02-15 22:40:03 +08:00
yunfan24andaias00 d5d2b6bc71 [doc] Update the SMS configuration document. (#3073)
Co-authored-by: aias00 <rokkki@163.com>
2025-02-13 20:32:48 +08:00
Logicandtomsun28 e11a4da2ac [improve](web-app): update monitor chart configuration and springboot GreptimeDB version (#3071)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-02-13 10:19:49 +08:00
58360442ca [Bugfix] fix when the monitor is modified, the status is erroneously changed (#3067)
Co-authored-by: aias00 <rokkki@163.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-02-13 00:14:37 +08:00
yunfan24andaias00 4d76c0abf4 [improve] Refactor SMS sending and replace Tencent Cloud SDK with HTTP API. (#3063)
Co-authored-by: aias00 <rokkki@163.com>
2025-02-11 19:40:36 +08:00
Jastandtomsun28 f761e18739 [Feature]Add Chinese check (#3066)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-02-11 10:36:55 +08:00
yunfan24 6ac073a54e [improve] Complete the missing labels in the i18n file. (#3065) 2025-02-11 10:16:36 +08:00
65a8d19c5d [feature] Support SSH Tunnel (#3060)
Co-authored-by: shown <yuluo08290126@gmail.com>
Co-authored-by: aias00 <rokkki@163.com>
2025-02-10 10:20:03 +08:00
tomsun28 060b4d875d [webapp] update ui theme (#3064)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-02-10 10:09:31 +08:00
kangliandLogic 6c88c1ba03 [API DOC] Change Swagger description (#3061)
Co-authored-by: Logic <zqr10159@dromara.org>
2025-02-08 19:29:12 +08:00
tomsun28 8c58a35476 [bugfix] fix singleton not support remove, search id error, audio fetch 401 (#3062)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-02-08 18:36:42 +08:00
2ab05a5669 [feature] Add Japanese i18n (#3013)
Co-authored-by: aias00 <rokkki@163.com>
Co-authored-by: Calvin <naruse_shinji@163.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: shown <yuluo08290126@gmail.com>
2025-02-08 09:19:13 +08:00
f3bc7f55c5 [feature] Add Sftp config for monitor (#3038)
Co-authored-by: yinyijun <yingey2011>
Co-authored-by: yunfan24 <yunfan24@outlook.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: yinyijun <yinyijun6@mgtv.com>
2025-02-07 23:57:06 +08:00
7effbc1f0d [Feature]Add zookeeper e2e code (#3030)
Co-authored-by: Calvin <naruse_shinji@163.com>
Co-authored-by: shown <yuluo08290126@gmail.com>
2025-02-07 23:22:10 +08:00
Logicandtomsun28 05e42182d9 [bugfix]style(alert-center): enhance 3D transformation and z-index layers (#3059)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
Signed-off-by: Logic <zqr10159@dromara.org>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-02-07 20:28:15 +08:00
yunfan24andtomsun28 5e69f4dfd0 [bugfix] Fix the issue where the monitoring status is not updated. (#3056)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-02-07 16:12:47 +08:00
tomsun28 aeb0083394 [improve] update theme ui color (#3057)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-02-07 15:47:19 +08:00
Logic 02f635bdd3 [improve](alert-center) enhance alert card animations and interactions (#3055) 2025-02-07 10:18:52 +08:00
137 changed files with 5403 additions and 1050 deletions
+5
View File
@@ -29,6 +29,11 @@
"type": 0,
"paramValue": 1000
},
{
"field": "ssl",
"type": 1,
"paramValue": false
},
{
"field": "username",
"type": 1
-19
View File
@@ -98,25 +98,6 @@
<version>${easy-poi.version}</version>
<scope>compile</scope>
</dependency>
<!-- sms -->
<dependency>
<groupId>com.tencentcloudapi</groupId>
<artifactId>tencentcloud-sdk-java-sms</artifactId>
<exclusions>
<exclusion>
<groupId>com.squareup.okhttp</groupId>
<artifactId>logging-interceptor</artifactId>
</exclusion>
<exclusion>
<groupId>com.squareup.okhttp</groupId>
<artifactId>okhttp</artifactId>
</exclusion>
<exclusion>
<groupId>com.squareup.okio</groupId>
<artifactId>okio</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.huaweicloud.sdk</groupId>
<artifactId>huaweicloud-sdk-smn</artifactId>
@@ -0,0 +1,46 @@
/*
* 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.config;
import lombok.Data;
/**
* Alibaba Cloud SMS configuration properties
*/
@Data
public class AlibabaSmsProperties {
/**
* Alibaba Cloud access key id
*/
private String accessKeyId;
/**
* Alibaba Cloud access key secret
*/
private String accessKeySecret;
/**
* SMS signature
*/
private String signName;
/**
* SMS template Code
*/
private String templateCode;
}
@@ -0,0 +1,57 @@
/*
* 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.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* SMS configuration
*/
@Data
@Component
@ConfigurationProperties(prefix = "alerter.sms")
public class SmsConfig {
/**
* whether to enable SMS, default is false
*/
private boolean enable = false;
/**
* sms service provider
*/
private String type;
/**
* Tencent cloud SMS configuration
*/
private TencentSmsProperties tencent;
/**
* Aliyun SMS configuration
*/
private AlibabaSmsProperties alibaba;
/**
* UniSMS configuration
*/
private UniSmsProperties unisms;
}
@@ -0,0 +1,51 @@
/*
* 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.config;
import lombok.Data;
/**
* Tencent Cloud SMS properties
*/
@Data
public class TencentSmsProperties {
/**
* Tencent cloud account secret id
*/
private String secretId;
/**
* Tencent cloud account secret key
*/
private String secretKey;
/**
* SMS app id
*/
private String appId;
/**
* SMS signature
*/
private String signName;
/**
* SMS template ID
*/
private String templateId;
}
@@ -0,0 +1,51 @@
/*
* 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.config;
import lombok.Data;
/**
* UniSMS properties
*/
@Data
public class UniSmsProperties {
/**
* UniSMS access key id
*/
private String accessKeyId;
/**
* UniSMS access key secret, required for HMAC mode
*/
private String accessKeySecret;
/**
* SMS signature
*/
private String signature;
/**
* SMS template ID
*/
private String templateId;
/**
* Authentication mode: simple or hmac, default is simple
*/
private String authMode = "simple";
}
@@ -38,9 +38,9 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Alarm Silence management API
* Alarm Inhibit management API
*/
@Tag(name = "Alert Silence API")
@Tag(name = "Alert Inhibit API")
@RestController
@RequestMapping(path = "/api/alert/inhibit", produces = {APPLICATION_JSON_VALUE})
public class AlertInhibitController {
@@ -49,7 +49,7 @@ public class AlertInhibitController {
private AlertInhibitService alertInhibitService;
@PostMapping
@Operation(summary = "New Alarm Silence", description = "Added an alarm Silence")
@Operation(summary = "New Alarm Inhibit", description = "Added an alarm Inhibit")
public ResponseEntity<Message<Void>> addNewAlertInhibit(@Valid @RequestBody AlertInhibit alertInhibit) {
alertInhibitService.validate(alertInhibit, false);
alertInhibitService.addAlertInhibit(alertInhibit);
@@ -57,7 +57,7 @@ public class AlertInhibitController {
}
@PutMapping
@Operation(summary = "Modifying an Alarm Silence", description = "Modify an existing alarm Silence")
@Operation(summary = "Modifying an Alarm Inhibit", description = "Modify an existing alarm Inhibit")
public ResponseEntity<Message<Void>> modifyAlertInhibit(@Valid @RequestBody AlertInhibit alertInhibit) {
alertInhibitService.validate(alertInhibit, true);
alertInhibitService.modifyAlertInhibit(alertInhibit);
@@ -65,10 +65,10 @@ public class AlertInhibitController {
}
@GetMapping(path = "/{id}")
@Operation(summary = "Querying Alarm Silence",
description = "You can obtain alarm Silence information based on the alarm Silence ID")
@Operation(summary = "Querying Alarm Inhibit",
description = "You can obtain alarm Inhibit information based on the alarm Inhibit ID")
public ResponseEntity<Message<AlertInhibit>> getAlertInhibit(
@Parameter(description = "Alarm Silence ID", example = "6565463543") @PathVariable("id") long id) {
@Parameter(description = "Alarm Inhibit ID", example = "6565463543") @PathVariable("id") long id) {
AlertInhibit alertInhibit = alertInhibitService.getAlertInhibit(id);
return Objects.isNull(alertInhibit)
@@ -36,9 +36,9 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* Silence the batch API for alarms
* Inhibit the batch API for alarms
*/
@Tag(name = "Alert Silence Batch API")
@Tag(name = "Alert Inhibit Batch API")
@RestController
@RequestMapping(path = "/api/alert/inhibits", produces = {APPLICATION_JSON_VALUE})
public class AlertInhibitsController {
@@ -50,7 +50,7 @@ public class AlertInhibitsController {
@Operation(summary = "Query the alarm inhibit list",
description = "You can obtain the list of alarm inhibit by querying filter items")
public ResponseEntity<Message<Page<AlertInhibit>>> getAlertInhibits(
@Parameter(description = "Alarm Silence ID", example = "6565463543") @RequestParam(required = false) List<Long> ids,
@Parameter(description = "Alarm Inhibit ID", example = "6565463543") @RequestParam(required = false) List<Long> ids,
@Parameter(description = "Search Name", example = "x") @RequestParam(required = false) String search,
@Parameter(description = "Sort field, default id", example = "id") @RequestParam(defaultValue = "id") String sort,
@Parameter(description = "Sort mode: asc: ascending, desc: descending", example = "desc") @RequestParam(defaultValue = "desc") String order,
@@ -64,7 +64,7 @@ public class AlertInhibitsController {
@Operation(summary = "Delete alarm inhibit in batches",
description = "Delete alarm inhibit in batches based on the alarm inhibit ID list")
public ResponseEntity<Message<Void>> deleteAlertDefines(
@Parameter(description = "Alarm Silence IDs", example = "6565463543") @RequestParam(required = false) List<Long> ids
@Parameter(description = "Alarm Inhibit IDs", example = "6565463543") @RequestParam(required = false) List<Long> ids
) {
if (ids != null && !ids.isEmpty()) {
alertInhibitService.deleteAlertInhibits(new HashSet<>(ids));
@@ -80,7 +80,7 @@ public class AlertNoticeDispatch {
if (noticeTemplate == null) {
noticeTemplate = noticeConfigService.getDefaultNoticeTemplateByType(alertNotifyHandler.type());
}
if (noticeTemplate == null) {
if (noticeTemplate == null && alertNotifyHandler.type() != 0) {
log.error("alert does not have mapping default notice template. type: {}.", alertNotifyHandler.type());
throw new NullPointerException(alertNotifyHandler.type() + " does not have mapping default notice template");
}
@@ -1,56 +0,0 @@
/*
* 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.notice.impl;
import java.util.ResourceBundle;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.notice.AlertNoticeException;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate;
import org.apache.hertzbeat.common.util.ResourceBundleUtil;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
/**
* Send alarm information through Alibaba Cloud SMS
*/
@Component
@RequiredArgsConstructor
@Slf4j
@ConditionalOnProperty("common.sms.aliyun.app-id")
final class AliYunAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl {
private final ResourceBundle bundle = ResourceBundleUtil.getBundle("alerter");
@Override
public void send(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) {
// SMS notification
try {
// todo send aliyun sms
} catch (Exception e) {
throw new AlertNoticeException("[Sms Notify Error] " + e.getMessage());
}
}
@Override
public byte type() {
return 0;
}
}
@@ -21,12 +21,12 @@ import java.util.ResourceBundle;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.notice.AlertNoticeException;
import org.apache.hertzbeat.alert.service.TencentSmsClient;
import org.apache.hertzbeat.alert.service.SmsClient;
import org.apache.hertzbeat.alert.service.SmsClientFactory;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate;
import org.apache.hertzbeat.common.util.ResourceBundleUtil;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
/**
@@ -35,35 +35,22 @@ import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
@Slf4j
@ConditionalOnProperty("common.sms.tencent.app-id")
@Deprecated
final class SmsAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl {
private final TencentSmsClient tencentSmsClient;
private final SmsClientFactory smsFactory;
private final ResourceBundle bundle = ResourceBundleUtil.getBundle("alerter");
@Override
public void send(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) {
// SMS notification todo use the rest api not sdk
try {
String instance = null;
String priority = null;
String content = null;
if (alert.getCommonLabels() != null) {
instance = alert.getCommonLabels().get("instance");
priority = alert.getCommonLabels().get("priority");
content = alert.getCommonAnnotations().get("summary");
content = content == null ? alert.getCommonAnnotations().get("description") : content;
if (content == null) {
content = alert.getCommonAnnotations().values().stream().findFirst().orElse(null);
}
SmsClient smsClient = smsFactory.getSmsClient();
if (smsClient == null) {
throw new AlertNoticeException("No SMS Service available, please check the configuration");
}
String[] params = new String[3];
params[0] = instance == null ? alert.getGroupKey() : instance;
params[1] = priority == null ? "unknown" : priority;
params[2] = content;
tencentSmsClient.sendMessage(params, new String[]{receiver.getPhone()});
if (!smsClient.checkConfig()) {
throw new AlertNoticeException(smsClient.getType() + " SMS Service configuration is invalid, please check the configuration");
}
smsClient.sendMessage(receiver, noticeTemplate, alert);
} catch (Exception e) {
throw new AlertNoticeException("[Sms Notify Error] " + e.getMessage());
}
@@ -80,7 +80,6 @@ final class WeChatAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl
JsonObject textContent = new JsonObject();
// Here you can construct the message content based on the NoticeTemplate and Alert information
// String alertMessage = String.format("警告:%s\n详情:%s", alert.getAlertDefineId(), alert.getContent());
String alertMessage = "Alert message content";
textContent.addProperty("content", alertMessage);
messageContent.add("text", textContent);
@@ -21,8 +21,8 @@ package org.apache.hertzbeat.alert.reduce;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@@ -267,7 +267,7 @@ public class AlarmGroupReduce {
.groupLabels(alert.getLabels())
.commonLabels(alert.getLabels())
.commonAnnotations(alert.getAnnotations())
.alerts(Collections.singletonList(alert))
.alerts(new LinkedList<>(List.of(alert)))
.status(alert.getStatus())
.build();
@@ -0,0 +1,42 @@
/*
* 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.service;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate;
/**
* SMS client interface
*/
public interface SmsClient {
/**
* send SMS
*/
void sendMessage(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert);
/**
* get SMS provider type
*/
String getType();
/**
* check SMS configuration, return true if the configuration is correct
*/
boolean checkConfig();
}
@@ -0,0 +1,141 @@
/*
* 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.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.config.SmsConfig;
import org.apache.hertzbeat.alert.service.impl.TencentSmsClientImpl;
import org.apache.hertzbeat.alert.service.impl.UniSmsClientImpl;
import org.apache.hertzbeat.alert.service.impl.AlibabaSmsClientImpl;
import org.apache.hertzbeat.base.dao.GeneralConfigDao;
import org.apache.hertzbeat.common.constants.GeneralConfigTypeEnum;
import org.apache.hertzbeat.common.entity.manager.GeneralConfig;
import org.apache.hertzbeat.common.support.event.SmsConfigChangeEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import static org.apache.hertzbeat.common.constants.SmsConstants.ALIBABA;
import static org.apache.hertzbeat.common.constants.SmsConstants.TENCENT;
import static org.apache.hertzbeat.common.constants.SmsConstants.UNISMS;
/**
* SMS client factory
*/
@Slf4j
@Component
public class SmsClientFactory {
private static final String TYPE = GeneralConfigTypeEnum.sms.name();
private final GeneralConfigDao generalConfigDao;
private final ObjectMapper objectMapper;
private final SmsConfig yamlSmsConfig;
private volatile SmsClient currentSmsClient;
public SmsClientFactory(GeneralConfigDao generalConfigDao,
ObjectMapper objectMapper,
SmsConfig yamlSmsConfig) {
this.generalConfigDao = generalConfigDao;
this.objectMapper = objectMapper;
this.yamlSmsConfig = yamlSmsConfig;
}
/**
* SMS configuration change event listener
*/
@EventListener(SmsConfigChangeEvent.class)
public void onSmsConfigChange(SmsConfigChangeEvent event) {
log.info("[SmsClientFactory] SMS configuration change event received");
synchronized (this) {
currentSmsClient = null;
}
}
public SmsClient getSmsClient() {
if (currentSmsClient != null) {
return currentSmsClient;
}
synchronized (this) {
if (currentSmsClient != null) {
return currentSmsClient;
}
loadConfig();
return currentSmsClient;
}
}
private void loadConfig() {
try {
// 1. try to load database configuration
SmsConfig dbConfig = loadDatabaseConfig();
if (dbConfig != null && !dbConfig.getType().isBlank() && dbConfig.isEnable()) {
createSmsClient(dbConfig);
if (currentSmsClient != null) {
log.info("[SmsClientFactory] Using database SMS configuration, provider: {}", dbConfig.getType());
return;
}
}
// 2. try to load YAML configuration
if (yamlSmsConfig != null && !yamlSmsConfig.getType().isBlank() && yamlSmsConfig.isEnable()) {
createSmsClient(yamlSmsConfig);
if (currentSmsClient != null) {
log.info("[SmsClientFactory] Using YAML SMS configuration, provider: {}", yamlSmsConfig.getType());
return;
}
}
log.warn("[SmsClientFactory] No valid SMS configuration found");
} catch (Exception e) {
log.error("[SmsClientFactory] Failed to load SMS configuration", e);
currentSmsClient = null;
}
}
private SmsConfig loadDatabaseConfig() {
try {
GeneralConfig config = generalConfigDao.findByType(TYPE);
if (config != null && config.getContent() != null) {
return objectMapper.readValue(config.getContent(), SmsConfig.class);
}
} catch (Exception e) {
log.error("[SmsClientFactory] Failed to load database configuration", e);
}
return null;
}
private void createSmsClient(SmsConfig smsConfig) {
switch (smsConfig.getType()) {
case TENCENT:
currentSmsClient = new TencentSmsClientImpl(smsConfig.getTencent());
break;
case UNISMS:
currentSmsClient = new UniSmsClientImpl(smsConfig.getUnisms());
break;
case ALIBABA:
currentSmsClient = new AlibabaSmsClientImpl(smsConfig.getAlibaba());
break;
default:
log.warn("[SmsClientFactory] Unsupported SMS provider type: {}", smsConfig.getType());
break;
}
}
}
@@ -1,101 +0,0 @@
/*
* 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.service;
import com.tencentcloudapi.common.Credential;
import com.tencentcloudapi.sms.v20210111.SmsClient;
import com.tencentcloudapi.sms.v20210111.models.SendSmsRequest;
import com.tencentcloudapi.sms.v20210111.models.SendSmsResponse;
import com.tencentcloudapi.sms.v20210111.models.SendStatus;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.common.config.CommonProperties;
import org.apache.hertzbeat.common.support.exception.SendMessageException;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
/**
* sms service client for tencent cloud
*/
@Component
@ConditionalOnProperty("common.sms.tencent.app-id")
@Slf4j
public class TencentSmsClient {
private static final String RESPONSE_OK = "Ok";
private static final String REGION = "ap-guangzhou";
private SmsClient smsClient;
private String appId;
private String signName;
private String templateId;
public TencentSmsClient(CommonProperties properties) {
if (properties == null || properties.getSms() == null || properties.getSms().getTencent() == null) {
log.error("init error, please config TencentSmsClient props in application.yml");
throw new IllegalArgumentException("please config TencentSmsClient props");
}
initSmsClient(properties.getSms().getTencent());
}
private void initSmsClient(CommonProperties.TencentSmsProperties tencent) {
this.appId = tencent.getAppId();
this.signName = tencent.getSignName();
this.templateId = tencent.getTemplateId();
Credential cred = new Credential(tencent.getSecretId(), tencent.getSecretKey());
smsClient = new SmsClient(cred, REGION);
}
/**
* send text message
* @param appId appId
* @param signName sign name
* @param templateId template id
* @param templateValues template values
* @param phones phones num
*/
public void sendMessage(String appId, String signName, String templateId,
String[] templateValues, String[] phones) {
SendSmsRequest req = new SendSmsRequest();
req.setSmsSdkAppId(appId);
req.setSignName(signName);
req.setTemplateId(templateId);
req.setTemplateParamSet(templateValues);
req.setPhoneNumberSet(phones);
try {
SendSmsResponse smsResponse = this.smsClient.SendSms(req);
SendStatus sendStatus = smsResponse.getSendStatusSet()[0];
if (!RESPONSE_OK.equals(sendStatus.getCode())) {
throw new SendMessageException(sendStatus.getCode() + ":" + sendStatus.getMessage());
}
} catch (Exception e) {
log.warn(e.getMessage());
throw new SendMessageException(e.getMessage());
}
}
/**
* send text message
* @param templateValues template values
* @param phones phones num
*/
public void sendMessage(String[] templateValues, String[] phones) {
sendMessage(this.appId, this.signName, this.templateId, templateValues, phones);
}
}
@@ -0,0 +1,234 @@
/*
* 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.service.impl;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.config.AlibabaSmsProperties;
import org.apache.hertzbeat.alert.service.SmsClient;
import org.apache.hertzbeat.alert.util.CryptoUtils;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
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.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 java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.SimpleTimeZone;
import java.util.TreeMap;
import java.util.UUID;
import static org.apache.hertzbeat.common.constants.SmsConstants.ALIBABA;
/**
* Alibaba Cloud SMS Client Implementation<br>
* API doc: <a href="https://next.api.aliyun.com/document/Dysmsapi/2017-05-25/SendSms">https://next.api.aliyun.com/document/Dysmsapi/2017-05-25/SendSms</a><br>
* Singnature doc: <a href="https://help.aliyun.com/zh/sdk/product-overview/v3-request-structure-and-signature">https://help.aliyun.com/zh/sdk/product-overview/v3-request-structure-and-signature</a>
*/
@Slf4j
public class AlibabaSmsClientImpl implements SmsClient {
private static final String API_VERSION = "2017-05-25";
private static final String ACTION = "SendSms";
private static final String HOST = "dysmsapi.aliyuncs.com";
private static final String ALGORITHM = "ACS3-HMAC-SHA256";
private final String accessKeyId;
private final String accessKeySecret;
private final String signName;
private final String templateCode;
public AlibabaSmsClientImpl(AlibabaSmsProperties config) {
if (config != null) {
this.accessKeyId = config.getAccessKeyId();
this.accessKeySecret = config.getAccessKeySecret();
this.signName = config.getSignName();
this.templateCode = config.getTemplateCode();
} else {
this.accessKeyId = "";
this.accessKeySecret = "";
this.signName = "";
this.templateCode = "";
}
}
@Override
public void sendMessage(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) {
// Extract alert info
String instance = null;
String priority = null;
String content = null;
if (alert.getCommonLabels() != null) {
instance = alert.getCommonLabels().get("instance");
priority = alert.getCommonLabels().get("priority");
content = alert.getCommonAnnotations().get("summary");
content = content == null ? alert.getCommonAnnotations().get("description") : content;
if (content == null) {
content = alert.getCommonAnnotations().values().stream().findFirst().orElse(null);
}
}
// Build template parameters
Map<String, String> templateParam = new HashMap<>();
templateParam.put("instance", instance == null ? alert.getGroupKey() : instance);
templateParam.put("priority", priority == null ? "unknown" : priority);
templateParam.put("content", content);
sendSms(receiver.getPhone(), JsonUtil.toJson(templateParam));
}
private void sendSms(String phoneNumber, String templateParam) {
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
// Build query parameters
Map<String, String> queryParams = new TreeMap<>();
queryParams.put("PhoneNumbers", phoneNumber);
queryParams.put("SignName", signName);
queryParams.put("TemplateCode", templateCode);
queryParams.put("TemplateParam", templateParam);
// Build canonical query string
StringBuilder canonicalQueryString = new StringBuilder();
queryParams.forEach((key, value) -> {
if (canonicalQueryString.length() > 0) {
canonicalQueryString.append("&");
}
canonicalQueryString.append(percentEncode(key))
.append("=")
.append(percentEncode(value));
});
// Generate timestamp and nonce
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
sdf.setTimeZone(new SimpleTimeZone(0, "GMT"));
String timestamp = sdf.format(new Date());
String nonce = UUID.randomUUID().toString();
// Calculate signature
String authorization = calculateAuthorization(
canonicalQueryString.toString(),
timestamp,
nonce
);
// Build URL
String url = "https://" + HOST + "/?" + canonicalQueryString;
// Build HTTP request
HttpPost httpPost = new HttpPost(url);
httpPost.setHeader("Content-Type", "application/json");
httpPost.setHeader("Host", HOST);
httpPost.setHeader("Authorization", authorization);
httpPost.setHeader("x-acs-action", ACTION);
httpPost.setHeader("x-acs-version", API_VERSION);
httpPost.setHeader("x-acs-date", timestamp);
httpPost.setHeader("x-acs-signature-nonce", nonce);
httpPost.setHeader("x-acs-content-sha256",
CryptoUtils.sha256Hex(""));
log.info("Sending Alibaba SMS request to {}", url + ", params: " + templateParam + "headers: " + Arrays.toString(httpPost.getAllHeaders()));
// Send request and handle response
try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
int statusCode = response.getStatusLine().getStatusCode();
String responseBody = EntityUtils.toString(response.getEntity());
log.info("SMS response status: {}, body: {}", statusCode, responseBody);
if (statusCode != 200) {
throw new SendMessageException("HTTP request failed with status code: " + statusCode + ", response: " + responseBody);
}
JsonNode jsonResponse = JsonUtil.fromJson(responseBody);
String code = jsonResponse.get("Code").asText();
if (!"OK".equals(code)) {
String message = jsonResponse.get("Message").asText();
throw new SendMessageException(code + ":" + message);
}
log.info("Successfully sent SMS to phone: {}", phoneNumber);
}
} catch (Exception e) {
log.warn("Failed to send SMS: {}", e.getMessage());
throw new SendMessageException(e.getMessage());
}
}
private String calculateAuthorization(String canonicalQueryString, String timestamp, String nonce) {
try {
// Step 1: Build canonical request
String canonicalRequest = buildCanonicalRequest(canonicalQueryString, timestamp, nonce);
// Step 2: Build string to sign
String stringToSign = ALGORITHM + "\n" + CryptoUtils.sha256Hex(canonicalRequest);
// Step 3: Calculate signature
String signature = CryptoUtils.hmacSha256Hex(accessKeySecret, stringToSign);
// 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) {
throw new RuntimeException("Failed to calculate authorization", e);
}
}
private String buildCanonicalRequest(String canonicalQueryString, String timestamp, String nonce) {
return "POST\n"
+ "/\n"
+ canonicalQueryString + "\n"
+ "host:" + HOST + "\n"
+ "x-acs-action:" + ACTION + "\n"
+ "x-acs-content-sha256:" + CryptoUtils.sha256Hex("") + "\n"
+ "x-acs-date:" + timestamp + "\n"
+ "x-acs-signature-nonce:" + nonce + "\n"
+ "x-acs-version:" + API_VERSION + "\n\n"
+ "host;x-acs-action;x-acs-content-sha256;x-acs-date;"
+ "x-acs-signature-nonce;x-acs-version\n"
+ CryptoUtils.sha256Hex("");
}
private String percentEncode(String value) {
try {
return java.net.URLEncoder.encode(value, StandardCharsets.UTF_8)
.replace("+", "%20")
.replace("*", "%2A")
.replace("%7E", "~");
} catch (Exception e) {
throw new RuntimeException("Failed to encode value", e);
}
}
@Override
public String getType() {
return ALIBABA;
}
@Override
public boolean checkConfig() {
return !(accessKeyId.isBlank() || accessKeySecret.isBlank() || signName.isBlank() || templateCode.isBlank());
}
}
@@ -0,0 +1,223 @@
/*
* 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.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.config.TencentSmsProperties;
import org.apache.hertzbeat.alert.service.SmsClient;
import org.apache.hertzbeat.alert.util.CryptoUtils;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
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.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.apache.hertzbeat.common.util.JsonUtil;
import com.fasterxml.jackson.databind.JsonNode;
import javax.xml.bind.DatatypeConverter;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.TimeZone;
import static org.apache.hertzbeat.common.constants.SmsConstants.TENCENT;
/**
* sms service client for tencent cloud <br>
* doc: <a href="https://cloud.tencent.com/document/api/382/55981">https://cloud.tencent.com/document/api/382/55981</a>
*/
@Slf4j
public class TencentSmsClientImpl implements SmsClient {
private static final String RESPONSE_OK = "Ok";
private static final String REGION = "ap-guangzhou";
private static final String API_VERSION = "2021-01-11";
private static final String ACTION = "SendSms";
private static final String HOST = "sms.tencentcloudapi.com";
private static final Charset UTF8 = StandardCharsets.UTF_8;
private String appId;
private String signName;
private String templateId;
private String secretId;
private String secretKey;
public TencentSmsClientImpl(TencentSmsProperties config) {
if (config != null) {
this.appId = config.getAppId();
this.signName = config.getSignName();
this.templateId = config.getTemplateId();
this.secretId = config.getSecretId();
this.secretKey = config.getSecretKey();
}
}
@Override
public void sendMessage(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) {
// todo limit the number of words
String instance = null;
String priority = null;
String content = null;
if (alert.getCommonLabels() != null) {
instance = alert.getCommonLabels().get("instance");
priority = alert.getCommonLabels().get("priority");
content = alert.getCommonAnnotations().get("summary");
content = content == null ? alert.getCommonAnnotations().get("description") : content;
if (content == null) {
content = alert.getCommonAnnotations().values().stream().findFirst().orElse(null);
}
}
String[] templateValues = new String[3];
templateValues[0] = instance == null ? alert.getGroupKey() : instance;
templateValues[1] = priority == null ? "unknown" : priority;
templateValues[2] = content;
String[] phones = new String[1];
phones[0] = receiver.getPhone();
sendSms(this.appId, this.signName, this.templateId, templateValues, phones);
}
public void sendSms(String appId, String signName, String templateId,
String[] templateValues, String[] phones) {
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
// build request payload
Map<String, Object> params = new HashMap<>();
params.put("SmsSdkAppId", appId);
params.put("SignName", signName);
params.put("TemplateId", templateId);
params.put("TemplateParamSet", templateValues);
params.put("PhoneNumberSet", phones);
String payload = JsonUtil.toJson(params);
// calculate request signature
String authorization = calculateAuthorization(
secretId, secretKey, "sms", HOST, REGION,
ACTION, API_VERSION, payload);
// build http request
HttpPost httpPost = new HttpPost("https://" + HOST);
httpPost.setHeader("Content-Type", "application/json; charset=utf-8");
httpPost.setHeader("Host", HOST);
httpPost.setHeader("X-TC-Action", ACTION);
httpPost.setHeader("X-TC-Timestamp", timestamp);
httpPost.setHeader("X-TC-Version", API_VERSION);
httpPost.setHeader("X-TC-Region", REGION);
httpPost.setHeader("Authorization", authorization);
httpPost.setEntity(new StringEntity(payload, StandardCharsets.UTF_8));
log.debug("Sending SMS request to {}, payload: {}", httpPost.getURI(), payload);
// send http request and handle response
try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
int statusCode = response.getStatusLine().getStatusCode();
String responseBody = EntityUtils.toString(response.getEntity());
log.debug("SMS response status: {}, body: {}", statusCode, responseBody);
if (statusCode != 200) {
throw new SendMessageException("HTTP request failed with status code: " + statusCode);
}
JsonNode jsonResponse = JsonUtil.fromJson(responseBody);
JsonNode responseNode = jsonResponse.get("Response");
JsonNode error = responseNode.get("Error");
if (error != null) {
String code = error.get("Code").asText();
String message = error.get("Message").asText();
throw new SendMessageException(code + ":" + message);
}
JsonNode sendStatusSet = responseNode.get("SendStatusSet");
if (sendStatusSet != null && sendStatusSet.isArray() && sendStatusSet.size() > 0) {
JsonNode firstStatus = sendStatusSet.get(0);
String code = firstStatus.get("Code").asText();
String message = firstStatus.get("Message").asText();
if (!RESPONSE_OK.equals(code)) {
throw new SendMessageException(code + ":" + message);
}
}
log.info("Successfully sent SMS to phones: {}", String.join(",", phones));
}
} catch (Exception e) {
log.warn("Failed to send SMS: {}", e.getMessage());
throw new SendMessageException(e.getMessage());
}
}
@Override
public String getType() {
return TENCENT;
}
@Override
public boolean checkConfig() {
if (appId.isBlank() || templateId.isBlank() || secretId.isBlank() || secretKey.isBlank()) {
return false;
}
return true;
}
public static String calculateAuthorization(String secretId, String secretKey,
String service, String host, String region,
String action, String version, String payload) throws Exception {
String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
String date = sdf.format(new Date(Long.valueOf(timestamp + "000")));
// Step 1: Construct the canonical request string
String httpRequestMethod = "POST";
String canonicalUri = "/";
String canonicalQueryString = "";
String canonicalHeaders = "content-type:application/json; charset=utf-8\n" + "host:" + host + "\n";
String signedHeaders = "content-type;host";
String hashedRequestPayload = CryptoUtils.sha256Hex(payload);
String canonicalRequest = httpRequestMethod + "\n" + canonicalUri + "\n" + canonicalQueryString + "\n"
+ canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestPayload;
// Step 2: Construct the string to sign
String algorithm = "TC3-HMAC-SHA256";
String credentialScope = date + "/" + service + "/" + "tc3_request";
String hashedCanonicalRequest = CryptoUtils.sha256Hex(canonicalRequest);
String stringToSign = algorithm + "\n" + timestamp + "\n" + credentialScope + "\n" + hashedCanonicalRequest;
// Step 3: Calculate the signature
byte[] secretDate = CryptoUtils.hmac256(("TC3" + secretKey).getBytes(UTF8), date);
byte[] secretService = CryptoUtils.hmac256(secretDate, service);
byte[] secretSigning = CryptoUtils.hmac256(secretService, "tc3_request");
String signature = DatatypeConverter.printHexBinary(
CryptoUtils.hmac256(secretSigning, stringToSign)).toLowerCase();
// Step 4: Construct the Authorization header
return algorithm + " " + "Credential=" + secretId + "/" + credentialScope + ", "
+ "SignedHeaders=" + signedHeaders + ", " + "Signature=" + signature;
}
}
@@ -0,0 +1,193 @@
/*
* 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.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.config.UniSmsProperties;
import org.apache.hertzbeat.alert.service.SmsClient;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
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.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import com.fasterxml.jackson.databind.JsonNode;
import org.apache.hertzbeat.alert.util.CryptoUtils;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
import java.util.UUID;
import java.util.stream.Collectors;
import static org.apache.hertzbeat.common.constants.SmsConstants.UNISMS;
/**
* UniSMS client implementation <br/>
* doc:<a href="https://unisms.apistd.com/docs/api/send">https://unisms.apistd.com/docs/api/send</a>
*/
@Slf4j
public class UniSmsClientImpl implements SmsClient {
private static final String API_URL = "https://uni.apistd.com";
private static final String ACTION = "sms.message.send";
private static final String SUCCESS_CODE = "0";
private static final String HMAC_ALGORITHM = "hmac-sha256";
private final UniSmsProperties config;
public UniSmsClientImpl(UniSmsProperties config) {
this.config = config;
}
@Override
public void sendMessage(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) {
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
// build request parameters
Map<String, Object> params = new HashMap<>();
params.put("to", receiver.getPhone());
params.put("signature", config.getSignature());
params.put("templateId", config.getTemplateId());
// build template data
Map<String, String> templateData = new HashMap<>();
String instance = alert.getCommonLabels().getOrDefault("instance", alert.getGroupKey());
String priority = alert.getCommonLabels().getOrDefault("priority", "unknown");
String content = alert.getCommonAnnotations().get("summary");
content = content == null ? alert.getCommonAnnotations().get("description") : content;
if (content == null) {
content = alert.getCommonAnnotations().values().stream().findFirst().orElse(null);
}
templateData.put("instance", instance);
templateData.put("priority", priority);
templateData.put("content", content);
params.put("templateData", templateData);
// build URL and request headers
String url;
if ("hmac".equalsIgnoreCase(config.getAuthMode())) {
url = buildHmacUrl();
} else {
url = buildSimpleUrl();
}
// send HTTP request
HttpPost httpPost = new HttpPost(url);
httpPost.setHeader("Content-Type", "application/json");
httpPost.setHeader("Accept", "application/json");
String payload = JsonUtil.toJson(params);
httpPost.setEntity(new StringEntity(payload, StandardCharsets.UTF_8));
log.info("Sending SMS request to UniSMS, payload: {}, url: {}", payload, url);
try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
handleResponse(response, receiver.getPhone());
}
} catch (Exception e) {
log.error("Failed to send SMS via UniSMS: {}", e.getMessage());
throw new SendMessageException(e.getMessage());
}
}
private String buildSimpleUrl() {
return String.format("%s/?action=%s&accessKeyId=%s",
API_URL, ACTION, config.getAccessKeyId());
}
private String buildHmacUrl() {
long timestamp = System.currentTimeMillis();
String nonce = generateNonce();
// build query parameters
Map<String, String> params = new TreeMap<>();
params.put("accessKeyId", config.getAccessKeyId());
params.put("action", ACTION);
params.put("algorithm", HMAC_ALGORITHM);
params.put("nonce", nonce);
params.put("timestamp", String.valueOf(timestamp));
// build sign text
String signText = params.entrySet().stream()
.map(entry -> entry.getKey() + "=" + entry.getValue())
.collect(Collectors.joining("&"));
// calculate signature
String signature = CryptoUtils.hmacSha256Base64(config.getAccessKeySecret(), signText);
return String.format("%s/?action=%s&accessKeyId=%s&algorithm=%s&timestamp=%d&nonce=%s&signature=%s",
API_URL, ACTION, config.getAccessKeyId(), HMAC_ALGORITHM, timestamp, nonce, signature);
}
private String generateNonce() {
return UUID.randomUUID().toString().replace("-", "").substring(0, 16);
}
private void handleResponse(CloseableHttpResponse response, String phone) throws IOException {
int statusCode = response.getStatusLine().getStatusCode();
String responseBody = EntityUtils.toString(response.getEntity());
log.info("UniSMS response status: {}, body: {}", statusCode, responseBody);
if (statusCode != 200) {
throw new SendMessageException("HTTP request failed with status code: " + statusCode + ", response: " + responseBody);
}
JsonNode jsonResponse = JsonUtil.fromJson(responseBody);
String code = jsonResponse.get("code").asText();
if (!SUCCESS_CODE.equals(code)) {
String message = jsonResponse.get("message").asText();
throw new SendMessageException(code + ":" + message);
}
log.info("Successfully sent SMS to phone: {}", phone);
}
@Override
public String getType() {
return UNISMS;
}
@Override
public boolean checkConfig() {
if (config == null
|| config.getAccessKeyId() == null
|| config.getAccessKeyId().isBlank()
|| config.getSignature() == null
|| config.getSignature().isBlank()
|| config.getTemplateId() == null
|| config.getTemplateId().isBlank()) {
return false;
}
// HMAC mode requires additional check for accessKeySecret
if ("hmac".equalsIgnoreCase(config.getAuthMode())
&& (config.getAccessKeySecret() == null || config.getAccessKeySecret().isBlank())) {
return false;
}
return true;
}
}
@@ -0,0 +1,95 @@
/*
* 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.util;
import lombok.extern.slf4j.Slf4j;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Base64;
/**
* General encryption utility class
*/
@Slf4j
public class CryptoUtils {
private static final Charset UTF8 = StandardCharsets.UTF_8;
private CryptoUtils() {}
/**
* Calculate HMAC-SHA256 signature
* @param key secret key
* @param msg message to be signed
* @return signed byte array
*/
public static byte[] hmac256(byte[] key, String msg) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKeySpec = new SecretKeySpec(key, mac.getAlgorithm());
mac.init(secretKeySpec);
return mac.doFinal(msg.getBytes(UTF8));
} catch (Exception e) {
log.error("Failed to calculate HMAC-SHA256: {}", e.getMessage());
throw new RuntimeException("Failed to calculate HMAC-SHA256", e);
}
}
/**
* Calculate SHA256 hash and convert to lowercase hexadecimal string
* @param data data to be hashed
* @return lowercase hexadecimal string
*/
public static String sha256Hex(String data) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] digest = md.digest(data.getBytes(UTF8));
return DatatypeConverter.printHexBinary(digest).toLowerCase();
} catch (Exception e) {
log.error("Failed to calculate SHA256: {}", e.getMessage());
throw new RuntimeException("Failed to calculate SHA256", e);
}
}
/**
* Calculate HMAC-SHA256 signature and convert to Base64 string
* @param key secret key
* @param data data to be signed
* @return Base64 encoded signature string
*/
public static String hmacSha256Base64(String key, String data) {
byte[] hmacResult = hmac256(key.getBytes(UTF8), data);
return Base64.getEncoder().encodeToString(hmacResult);
}
/**
* Calculate HMAC-SHA256 signature and convert to lowercase hexadecimal string
* @param key secret key
* @param data data to be signed
* @return lowercase hexadecimal string
*/
public static String hmacSha256Hex(String key, String data) {
byte[] hmacResult = hmac256(key.getBytes(UTF8), data);
return DatatypeConverter.printHexBinary(hmacResult).toLowerCase();
}
}
@@ -99,7 +99,7 @@ class EmailAlertNotifyHandlerImplTest {
template.setName("test-template");
template.setContent("test content");
// 设置邮件服务器配置
// Set up email server configuration
MailServerConfig mailServerConfig = new MailServerConfig();
mailServerConfig.setEmailHost("smtp.example.com");
mailServerConfig.setEmailPort(587);
@@ -83,8 +83,8 @@ class AlarmInhibitReduceTest {
MockitoAnnotations.openMocks(this);
when(alertInhibitDao.findAlertInhibitsByEnableIsTrue())
.thenReturn(Collections.emptyList());
// 正确设置 AlerterProperties mock
// Correctly set up AlerterProperties mock
AlerterProperties.InhibitProperties inhibitProperties = new AlerterProperties.InhibitProperties();
inhibitProperties.setTtl(60000);
when(alerterProperties.getInhibit()).thenReturn(inhibitProperties);
@@ -155,5 +155,9 @@
<artifactId>plc4j-driver-modbus</artifactId>
<version>0.12.0</version>
</dependency>
<dependency>
<groupId>org.apache.sshd</groupId>
<artifactId>sshd-sftp</artifactId>
</dependency>
</dependencies>
</project>
@@ -26,20 +26,25 @@ import java.util.HashMap;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
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;
import org.apache.hertzbeat.collector.collect.common.cache.GlobalConnectionCache;
import org.apache.hertzbeat.collector.collect.common.cache.JdbcConnect;
import org.apache.hertzbeat.collector.collect.common.ssh.SshTunnelHelper;
import org.apache.hertzbeat.collector.constants.CollectorConstants;
import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
import org.apache.hertzbeat.collector.util.CollectUtil;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.job.Metrics;
import org.apache.hertzbeat.common.entity.job.SshTunnel;
import org.apache.hertzbeat.common.entity.job.protocol.JdbcProtocol;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.util.CommonUtil;
import org.apache.sshd.common.SshException;
import org.apache.sshd.common.channel.exception.SshChannelOpenException;
import org.postgresql.util.PSQLException;
import org.springframework.core.io.FileSystemResource;
import org.springframework.jdbc.datasource.init.ScriptUtils;
@@ -55,7 +60,7 @@ public class JdbcCommonCollect extends AbstractCollect {
private static final String QUERY_TYPE_MULTI_ROW = "multiRow";
private static final String QUERY_TYPE_COLUMNS = "columns";
private static final String RUN_SCRIPT = "runScript";
private static final String[] VULNERABLE_KEYWORDS = {"allowLoadLocalInfile", "allowLoadLocalInfileInPath", "useLocalInfile"};
private final GlobalConnectionCache connectionCommonCache = GlobalConnectionCache.getInstance();
@@ -73,16 +78,26 @@ public class JdbcCommonCollect extends AbstractCollect {
}
}
}
SshTunnelHelper.checkTunnelParam(metrics.getJdbc().getSshTunnel());
}
@Override
public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) {
long startTime = System.currentTimeMillis();
JdbcProtocol jdbcProtocol = metrics.getJdbc();
String databaseUrl = constructDatabaseUrl(jdbcProtocol);
SshTunnel sshTunnel = jdbcProtocol.getSshTunnel();
int timeout = CollectUtil.getTimeout(jdbcProtocol.getTimeout());
Statement statement = null;
String databaseUrl;
try {
if (sshTunnel != null && Boolean.parseBoolean(sshTunnel.getEnable())) {
int localPort = SshTunnelHelper.localPortForward(sshTunnel, jdbcProtocol.getHost(), jdbcProtocol.getPort());
databaseUrl = constructDatabaseUrl(jdbcProtocol, "localhost", String.valueOf(localPort));
} else {
databaseUrl = constructDatabaseUrl(jdbcProtocol, jdbcProtocol.getHost(), jdbcProtocol.getPort());
}
statement = getConnection(jdbcProtocol.getUsername(),
jdbcProtocol.getPassword(), databaseUrl, timeout);
switch (jdbcProtocol.getQueryType()) {
@@ -112,6 +127,14 @@ public class JdbcCommonCollect extends AbstractCollect {
log.warn("Jdbc sql error: {}, code: {}.", sqlException.getMessage(), sqlException.getErrorCode());
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg("Query Error: " + sqlException.getMessage() + " Code: " + sqlException.getErrorCode());
} catch (SshException sshException) {
Throwable throwable = sshException.getCause();
if (throwable instanceof SshChannelOpenException) {
log.warn("[Jdbc collect] Remote ssh server no more session channel, please increase sshd_config MaxSessions.");
}
String errorMsg = CommonUtil.getMessageFromThrowable(sshException);
builder.setCode(CollectRep.Code.UN_CONNECTABLE);
builder.setMsg("Peer ssh connection failed: " + errorMsg);
} catch (Exception e) {
String errorMessage = CommonUtil.getMessageFromThrowable(e);
log.error("Jdbc error: {}.", errorMessage, e);
@@ -185,13 +208,14 @@ public class JdbcCommonCollect extends AbstractCollect {
* eg:
* query metricsone tow three four
* query sqlselect one, tow, three, four from book limit 1;
*
* @param statement statement
* @param sql sql
* @param columns query metrics field list
* @param sql sql
* @param columns query metrics field list
* @throws Exception when error happen
*/
private void queryOneRow(Statement statement, String sql, List<String> columns,
CollectRep.MetricsData.Builder builder, long startTime) throws Exception {
CollectRep.MetricsData.Builder builder, long startTime) throws Exception {
statement.setMaxRows(1);
try (ResultSet resultSet = statement.executeQuery(sql)) {
if (resultSet.next()) {
@@ -216,14 +240,15 @@ public class JdbcCommonCollect extends AbstractCollect {
* eg:
* query metricsone two three four
* query sqlselect key, value from book; the key is the query metrics fields
* select key, value from book;
* select key, value from book;
* one - value1
* two - value2
* three - value3
* four - value4
*
* @param statement statement
* @param sql sql
* @param columns query metrics field list
* @param sql sql
* @param columns query metrics field list
* @throws Exception when error happen
*/
private void queryOneRowByMatchTwoColumns(Statement statement, String sql, List<String> columns,
@@ -256,9 +281,10 @@ public class JdbcCommonCollect extends AbstractCollect {
* query metricsone tow three four
* query sqlselect one, tow, three, four from book;
* and return multi row record mapping with the metrics
*
* @param statement statement
* @param sql sql
* @param columns query metrics field list
* @param sql sql
* @param columns query metrics field list
* @throws Exception when error happen
*/
private void queryMultiRow(Statement statement, String sql, List<String> columns,
@@ -283,14 +309,16 @@ public class JdbcCommonCollect extends AbstractCollect {
/**
* construct jdbc url due the jdbc protocol
*
* @param jdbcProtocol jdbc
* @return URL
*/
private String constructDatabaseUrl(JdbcProtocol jdbcProtocol) {
private String constructDatabaseUrl(JdbcProtocol jdbcProtocol, String host, String port) {
if (Objects.nonNull(jdbcProtocol.getUrl())
&& !Objects.equals("", jdbcProtocol.getUrl())
&& jdbcProtocol.getUrl().startsWith("jdbc")) {
String url = jdbcProtocol.getUrl().toLowerCase(); // convert the URL to lowercase for case-insensitive checking
// convert the URL to lowercase for case-insensitive checking
String url = jdbcProtocol.getUrl().toLowerCase();
// check whether the parameter is valid
if (url.contains("create trigger") || url.contains("create alias") || url.contains("runscript from")
|| url.contains("allowloadlocalinfile") || url.contains("allowloadlocalinfileinpath")
@@ -302,25 +330,19 @@ public class JdbcCommonCollect extends AbstractCollect {
return jdbcProtocol.getUrl();
}
return switch (jdbcProtocol.getPlatform()) {
case "mysql", "mariadb" ->
"jdbc:mysql://" + jdbcProtocol.getHost() + ":" + jdbcProtocol.getPort()
case "mysql", "mariadb" -> "jdbc:mysql://" + host + ":" + port
+ "/" + (jdbcProtocol.getDatabase() == null ? "" : jdbcProtocol.getDatabase())
+ "?useUnicode=true&characterEncoding=utf-8&useSSL=false";
case "postgresql" ->
"jdbc:postgresql://" + jdbcProtocol.getHost() + ":" + jdbcProtocol.getPort()
case "postgresql" -> "jdbc:postgresql://" + host + ":" + port
+ "/" + (jdbcProtocol.getDatabase() == null ? "" : jdbcProtocol.getDatabase());
case "clickhouse" ->
"jdbc:clickhouse://" + jdbcProtocol.getHost() + ":" + jdbcProtocol.getPort()
case "clickhouse" -> "jdbc:clickhouse://" + host + ":" + port
+ "/" + (jdbcProtocol.getDatabase() == null ? "" : jdbcProtocol.getDatabase());
case "sqlserver" ->
"jdbc:sqlserver://" + jdbcProtocol.getHost() + ":" + jdbcProtocol.getPort()
case "sqlserver" -> "jdbc:sqlserver://" + host + ":" + port
+ ";" + (jdbcProtocol.getDatabase() == null ? "" : "DatabaseName=" + jdbcProtocol.getDatabase())
+ ";trustServerCertificate=true;";
case "oracle" ->
"jdbc:oracle:thin:@" + jdbcProtocol.getHost() + ":" + jdbcProtocol.getPort()
case "oracle" -> "jdbc:oracle:thin:@" + host + ":" + port
+ "/" + (jdbcProtocol.getDatabase() == null ? "" : jdbcProtocol.getDatabase());
case "dm" ->
"jdbc:dm://" + jdbcProtocol.getHost() + ":" + jdbcProtocol.getPort();
case "dm" -> "jdbc:dm://" + host + ":" + port;
default -> throw new IllegalArgumentException("Not support database platform: " + jdbcProtocol.getPlatform());
};
}
@@ -17,9 +17,11 @@
package org.apache.hertzbeat.collector.collect.ftp;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.hertzbeat.collector.collect.AbstractCollect;
@@ -29,6 +31,10 @@ import org.apache.hertzbeat.common.entity.job.Metrics;
import org.apache.hertzbeat.common.entity.job.protocol.FtpProtocol;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.util.CommonUtil;
import org.apache.sshd.client.SshClient;
import org.apache.sshd.client.session.ClientSession;
import org.apache.sshd.sftp.client.SftpClient;
import org.apache.sshd.sftp.client.SftpClientFactory;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -56,33 +62,14 @@ public class FtpCollectImpl extends AbstractCollect {
Assert.hasText(ftpProtocol.getTimeout(), "Ftp Protocol timeout is required.");
}
@Override
public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) {
FTPClient ftpClient = new FTPClient();
FtpProtocol ftpProtocol = metrics.getFtp();
// Set timeout
ftpClient.setControlKeepAliveReplyTimeout(Integer.parseInt(ftpProtocol.getTimeout()));
// Collect data to load in CollectRep.ValueRow.Builder's object
CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder();
Map<String, String> valueMap;
try {
valueMap = collectValue(ftpClient, ftpProtocol);
metrics.getAliasFields().forEach(it -> {
if (valueMap.containsKey(it)) {
String fieldValue = valueMap.get(it);
valueRowBuilder.addColumn(Objects.requireNonNullElse(fieldValue, CommonConstants.NULL_VALUE));
} else {
valueRowBuilder.addColumn(CommonConstants.NULL_VALUE);
}
});
} catch (Exception e) {
builder.setCode(CollectRep.Code.UN_CONNECTABLE);
builder.setMsg(e.getMessage());
return;
boolean ssl = Boolean.parseBoolean(metrics.getFtp().getSsl());
if (ssl){
handleSftpCollect(builder, metrics);
} else {
handleFtpCollect(builder, metrics);
}
builder.addValueRow(valueRowBuilder.build());
}
/**
@@ -113,8 +100,23 @@ public class FtpCollectImpl extends AbstractCollect {
};
}
private Map<String, String> collectValue(SftpClient sftpClient, FtpProtocol ftpProtocol) {
boolean isActive;
String responseTime;
try {
long startTime = System.currentTimeMillis();
sftpClient.stat(ftpProtocol.getDirection());
isActive = true;
long endTime = System.currentTimeMillis();
responseTime = String.valueOf(endTime - startTime);
} catch (IOException e) {
throw new IllegalArgumentException("[SFTPClient] error: {}" + CommonUtil.getMessageFromThrowable(e), e);
}
return Map.of("isActive", Boolean.toString(isActive), "responseTime", responseTime);
}
/**
* login
* ftp login
*/
private void login(FTPClient ftpClient, FtpProtocol ftpProtocol) {
try {
@@ -147,8 +149,84 @@ public class FtpCollectImpl extends AbstractCollect {
}
}
private ClientSession connect(SshClient client, FtpProtocol ftpProtocol) {
client.start();
try {
ClientSession session = client.connect(ftpProtocol.getUsername(), ftpProtocol.getHost(), Integer.parseInt(ftpProtocol.getPort()))
.verify(Integer.parseInt(ftpProtocol.getTimeout()))
.getSession();
session.addPasswordIdentity(ftpProtocol.getPassword());
session.auth().verify(Integer.parseInt(ftpProtocol.getTimeout()));
return session;
} catch (Exception e) {
throw new IllegalArgumentException("[sftp connection] error: {}" + CommonUtil.getMessageFromThrowable(e), e);
}
}
@Override
public String supportProtocol() {
return DispatchConstants.PROTOCOL_FTP;
}
}
private void handleFtpCollect(CollectRep.MetricsData.Builder builder, Metrics metrics) {
FTPClient ftpClient = new FTPClient();
FtpProtocol ftpProtocol = metrics.getFtp();
// Set timeout
ftpClient.setControlKeepAliveReplyTimeout(Integer.parseInt(ftpProtocol.getTimeout()));
// Collect data to load in CollectRep.ValueRow.Builder's object
CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder();
Map<String, String> valueMap;
try {
valueMap = collectValue(ftpClient, ftpProtocol);
metrics.getAliasFields().forEach(it -> {
if (valueMap.containsKey(it)) {
String fieldValue = valueMap.get(it);
valueRowBuilder.addColumn(Objects.requireNonNullElse(fieldValue, CommonConstants.NULL_VALUE));
} else {
valueRowBuilder.addColumn(CommonConstants.NULL_VALUE);
}
});
} catch (Exception e) {
builder.setCode(CollectRep.Code.UN_CONNECTABLE);
builder.setMsg(e.getMessage());
return;
}
builder.addValueRow(valueRowBuilder.build());
}
private void handleSftpCollect(CollectRep.MetricsData.Builder builder, Metrics metrics) {
FtpProtocol ftpProtocol = metrics.getFtp();
ClientSession session = null;
SftpClient sftpClient = null;
SshClient client = null;
try {
client = SshClient.setUpDefaultClient();
session = connect(client, ftpProtocol);
sftpClient = SftpClientFactory.instance().createSftpClient(session);
Map<String, String> valueMap = collectValue(sftpClient, ftpProtocol);
CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder();
metrics.getAliasFields().forEach(it ->
valueRowBuilder.addColumn(valueMap.getOrDefault(it, CommonConstants.NULL_VALUE))
);
builder.addValueRow(valueRowBuilder.build());
} catch (Exception e) {
builder.setCode(CollectRep.Code.UN_CONNECTABLE);
builder.setMsg(e.getMessage());
} finally {
try {
if (sftpClient != null && sftpClient.isOpen()){
sftpClient.close();
}
if (session != null && session.isOpen()){
session.close();
}
if (client != null && client.isOpen()){
client.close();
}
} catch (Exception e){
log.error("[SFTPClient] error while closing: {}", CommonUtil.getMessageFromThrowable(e), e);
}
}
}
}
@@ -28,6 +28,8 @@ import io.lettuce.core.cluster.models.partitions.Partitions;
import io.lettuce.core.cluster.models.partitions.RedisClusterNode;
import io.lettuce.core.resource.ClientResources;
import io.lettuce.core.resource.DefaultClientResources;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
@@ -42,6 +44,7 @@ import org.apache.hertzbeat.collector.collect.common.cache.AbstractConnection;
import org.apache.hertzbeat.collector.collect.common.cache.CacheIdentifier;
import org.apache.hertzbeat.collector.collect.common.cache.GlobalConnectionCache;
import org.apache.hertzbeat.collector.collect.common.cache.RedisConnect;
import org.apache.hertzbeat.collector.collect.common.ssh.SshTunnelHelper;
import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
import org.apache.hertzbeat.collector.util.CollectUtil;
import org.apache.hertzbeat.common.constants.CommonConstants;
@@ -51,6 +54,8 @@ import org.apache.hertzbeat.common.entity.job.protocol.RedisProtocol;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.util.CommonUtil;
import org.apache.hertzbeat.common.util.MapCapUtil;
import org.apache.sshd.common.SshException;
import org.apache.sshd.common.channel.exception.SshChannelOpenException;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -79,6 +84,7 @@ public class RedisCommonCollectImpl extends AbstractCollect {
RedisProtocol redisProtocol = metrics.getRedis();
Assert.hasText(redisProtocol.getHost(), "Redis Protocol host is required.");
Assert.hasText(redisProtocol.getPort(), "Redis Protocol port is required.");
SshTunnelHelper.checkTunnelParam(metrics.getRedis().getSshTunnel());
}
@Override
@@ -96,6 +102,14 @@ public class RedisCommonCollectImpl extends AbstractCollect {
log.info("[redis connection] error: {}", errorMsg);
builder.setCode(CollectRep.Code.UN_CONNECTABLE);
builder.setMsg(errorMsg);
} catch (SshException sshException) {
Throwable throwable = sshException.getCause();
if (throwable instanceof SshChannelOpenException) {
log.warn("[redis collect] Remote ssh server no more session channel, please increase sshd_config MaxSessions.");
}
String errorMsg = CommonUtil.getMessageFromThrowable(sshException);
builder.setCode(CollectRep.Code.UN_CONNECTABLE);
builder.setMsg("Peer ssh connection failed: " + errorMsg);
} catch (Exception e) {
String errorMsg = CommonUtil.getMessageFromThrowable(e);
log.warn("[redis collect] error: {}", e.getMessage(), e);
@@ -109,7 +123,7 @@ public class RedisCommonCollectImpl extends AbstractCollect {
* @param metrics metrics config
* @return data
*/
private Map<String, String> getSingleRedisInfo(Metrics metrics) {
private Map<String, String> getSingleRedisInfo(Metrics metrics) throws GeneralSecurityException, IOException {
StatefulRedisConnection<String, String> connection = getSingleConnection(metrics.getRedis());
String info = connection.sync().info(metrics.getName());
Map<String, String> valueMap = parseInfo(info, metrics);
@@ -125,7 +139,7 @@ public class RedisCommonCollectImpl extends AbstractCollect {
* @param metrics metrics config
* @return data
*/
private List<Map<String, String>> getClusterRedisInfo(Metrics metrics) {
private List<Map<String, String>> getClusterRedisInfo(Metrics metrics) throws GeneralSecurityException, IOException {
Map<String, StatefulRedisClusterConnection<String, String>> connectionMap = getConnectionList(metrics.getRedis());
List<Map<String, String>> list = new ArrayList<>(connectionMap.size());
connectionMap.forEach((identity, connection) ->{
@@ -179,12 +193,16 @@ public class RedisCommonCollectImpl extends AbstractCollect {
* @param redisProtocol protocol
* @return connection
*/
private StatefulRedisConnection<String, String> getSingleConnection(RedisProtocol redisProtocol) {
CacheIdentifier identifier = doIdentifier(redisProtocol);
private StatefulRedisConnection<String, String> getSingleConnection(RedisProtocol redisProtocol) throws GeneralSecurityException, IOException {
String[] resolvedArr = resolveHostAndPort(redisProtocol);
String host = resolvedArr[0];
String port = resolvedArr[1];
CacheIdentifier identifier = doIdentifier(redisProtocol, host, port);
StatefulRedisConnection<String, String> connection = (StatefulRedisConnection<String, String>) getStatefulConnection(identifier);
if (Objects.isNull(connection)) {
// reuse connection failed, new one
RedisClient redisClient = buildSingleClient(redisProtocol);
RedisClient redisClient = buildSingleClient(redisProtocol, host, port);
connection = redisClient.connect();
connectionCache.addCache(identifier, new RedisConnect(connection));
}
@@ -196,7 +214,7 @@ public class RedisCommonCollectImpl extends AbstractCollect {
* @param redisProtocol protocol
* @return connection map
*/
private Map<String, StatefulRedisClusterConnection<String, String>> getConnectionList(RedisProtocol redisProtocol) {
private Map<String, StatefulRedisClusterConnection<String, String>> getConnectionList(RedisProtocol redisProtocol) throws GeneralSecurityException, IOException {
// first connection
StatefulRedisClusterConnection<String, String> connection = getClusterConnection(redisProtocol);
Partitions partitions = connection.getPartitions();
@@ -217,12 +235,16 @@ public class RedisCommonCollectImpl extends AbstractCollect {
* @param redisProtocol redis protocol
* @return cluster connection
*/
private StatefulRedisClusterConnection<String, String> getClusterConnection(RedisProtocol redisProtocol) {
CacheIdentifier identifier = doIdentifier(redisProtocol);
private StatefulRedisClusterConnection<String, String> getClusterConnection(RedisProtocol redisProtocol) throws GeneralSecurityException, IOException {
String[] resolvedArr = resolveHostAndPort(redisProtocol);
String host = resolvedArr[0];
String port = resolvedArr[1];
CacheIdentifier identifier = doIdentifier(redisProtocol, host, port);
StatefulRedisClusterConnection<String, String> connection = (StatefulRedisClusterConnection<String, String>) getStatefulConnection(identifier);
if (connection == null) {
// reuse connection failed, new one
RedisClusterClient redisClusterClient = buildClusterClient(redisProtocol);
RedisClusterClient redisClusterClient = buildClusterClient(redisProtocol, host, port);
connection = redisClusterClient.connect();
connectionCache.addCache(identifier, new RedisConnect(connection));
}
@@ -260,8 +282,8 @@ public class RedisCommonCollectImpl extends AbstractCollect {
* @param redisProtocol redis protocol config
* @return redis cluster client
*/
private RedisClusterClient buildClusterClient(RedisProtocol redisProtocol) {
return RedisClusterClient.create(defaultClientResources, redisUri(redisProtocol));
private RedisClusterClient buildClusterClient(RedisProtocol redisProtocol, String host, String port) {
return RedisClusterClient.create(defaultClientResources, redisUri(redisProtocol, host, port));
}
/**
@@ -270,12 +292,12 @@ public class RedisCommonCollectImpl extends AbstractCollect {
* @param redisProtocol redis protocol config
* @return redis single client
*/
private RedisClient buildSingleClient(RedisProtocol redisProtocol) {
return RedisClient.create(defaultClientResources, redisUri(redisProtocol));
private RedisClient buildSingleClient(RedisProtocol redisProtocol, String host, String port) {
return RedisClient.create(defaultClientResources, redisUri(redisProtocol, host, port));
}
private RedisURI redisUri(RedisProtocol redisProtocol) {
RedisURI.Builder redisUriBuilder = RedisURI.builder().withHost(redisProtocol.getHost()).withPort(Integer.parseInt(redisProtocol.getPort()));
private RedisURI redisUri(RedisProtocol redisProtocol, String host, String port) {
RedisURI.Builder redisUriBuilder = RedisURI.builder().withHost(host).withPort(Integer.parseInt(port));
if (StringUtils.hasText(redisProtocol.getUsername())) {
redisUriBuilder.withClientName(redisProtocol.getUsername());
}
@@ -295,10 +317,10 @@ public class RedisCommonCollectImpl extends AbstractCollect {
return ip + SignConstants.DOUBLE_MARK + port;
}
private CacheIdentifier doIdentifier(RedisProtocol redisProtocol) {
private CacheIdentifier doIdentifier(RedisProtocol redisProtocol, String host, String port) {
return CacheIdentifier.builder()
.ip(redisProtocol.getHost())
.port(redisProtocol.getPort())
.ip(host)
.port(port)
.username(redisProtocol.getUsername())
.password(redisProtocol.getPassword())
.customArg(redisProtocol.getPattern())
@@ -328,6 +350,22 @@ public class RedisCommonCollectImpl extends AbstractCollect {
return result;
}
private String[] resolveHostAndPort(RedisProtocol redisProtocol) throws GeneralSecurityException, IOException {
boolean enableSshTunnel = Optional.ofNullable(redisProtocol.getSshTunnel())
.map(ssh -> Boolean.parseBoolean(ssh.getEnable()))
.orElse(false);
String host;
String port;
if (enableSshTunnel){
host = "localhost";
port = String.valueOf(SshTunnelHelper.localPortForward(redisProtocol.getSshTunnel(), redisProtocol.getHost(), redisProtocol.getPort()));
} else {
host = redisProtocol.getHost();
port = redisProtocol.getPort();
}
return new String[]{host, port};
}
@Override
public String supportProtocol() {
return DispatchConstants.PROTOCOL_REDIS;
@@ -18,7 +18,6 @@
package org.apache.hertzbeat.collector.collect.ssh;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.ConnectException;
import java.net.SocketTimeoutException;
@@ -30,35 +29,27 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
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;
import org.apache.hertzbeat.collector.collect.common.cache.GlobalConnectionCache;
import org.apache.hertzbeat.collector.collect.common.cache.SshConnect;
import org.apache.hertzbeat.collector.collect.common.ssh.CommonSshBlacklist;
import org.apache.hertzbeat.collector.collect.common.ssh.CommonSshClient;
import org.apache.hertzbeat.collector.collect.common.ssh.SshHelper;
import org.apache.hertzbeat.collector.constants.CollectorConstants;
import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
import org.apache.hertzbeat.collector.util.CollectUtil;
import org.apache.hertzbeat.collector.util.PrivateKeyUtils;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.job.Metrics;
import org.apache.hertzbeat.common.entity.job.protocol.SshProtocol;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.util.CommonUtil;
import org.apache.sshd.client.SshClient;
import org.apache.sshd.client.channel.ClientChannel;
import org.apache.sshd.client.channel.ClientChannelEvent;
import org.apache.sshd.client.session.ClientSession;
import org.apache.sshd.common.SshException;
import org.apache.sshd.common.channel.exception.SshChannelOpenException;
import org.apache.sshd.common.config.keys.FilePasswordProvider;
import org.apache.sshd.common.util.io.output.NoCloseOutputStream;
import org.apache.sshd.common.util.security.SecurityUtils;
import org.springframework.util.StringUtils;
/**
@@ -291,57 +282,9 @@ public class SshCollectImpl extends AbstractCollect {
private ClientSession getConnectSession(SshProtocol sshProtocol, int timeout, boolean reuseConnection)
throws IOException, GeneralSecurityException {
CacheIdentifier identifier = CacheIdentifier.builder()
.ip(sshProtocol.getHost()).port(sshProtocol.getPort())
.username(sshProtocol.getUsername()).password(sshProtocol.getPassword())
.build();
ClientSession clientSession = null;
if (reuseConnection) {
Optional<AbstractConnection<?>> cacheOption = connectionCommonCache.getCache(identifier, true);
if (cacheOption.isPresent()) {
SshConnect sshConnect = (SshConnect) cacheOption.get();
clientSession = sshConnect.getConnection();
try {
if (clientSession == null || clientSession.isClosed() || clientSession.isClosing()) {
clientSession = null;
connectionCommonCache.removeCache(identifier);
}
} catch (Exception e) {
log.warn(e.getMessage());
clientSession = null;
connectionCommonCache.removeCache(identifier);
}
}
if (clientSession != null) {
return clientSession;
}
}
SshClient sshClient = CommonSshClient.getSshClient();
clientSession = sshClient.connect(sshProtocol.getUsername(), sshProtocol.getHost(), Integer.parseInt(sshProtocol.getPort()))
.verify(timeout, TimeUnit.MILLISECONDS).getSession();
if (StringUtils.hasText(sshProtocol.getPassword())) {
clientSession.addPasswordIdentity(sshProtocol.getPassword());
} else if (StringUtils.hasText(sshProtocol.getPrivateKey())) {
var resourceKey = PrivateKeyUtils.writePrivateKey(sshProtocol.getHost(), sshProtocol.getPrivateKey());
FilePasswordProvider passwordProvider = (session, resource, index) -> {
if (StringUtils.hasText(sshProtocol.getPrivateKeyPassphrase())) {
return sshProtocol.getPrivateKeyPassphrase();
}
return null;
};
SecurityUtils.loadKeyPairIdentities(null, () -> resourceKey, new FileInputStream(resourceKey), passwordProvider)
.forEach(clientSession::addPublicKeyIdentity);
} // else auth with localhost private public key certificates
// auth
if (!clientSession.auth().verify(timeout, TimeUnit.MILLISECONDS).isSuccess()) {
clientSession.close();
throw new IllegalArgumentException("ssh auth failed.");
}
if (reuseConnection) {
SshConnect sshConnect = new SshConnect(clientSession);
connectionCommonCache.addCache(identifier, sshConnect);
}
return clientSession;
return SshHelper.getConnectSession(
sshProtocol.getHost(), sshProtocol.getPort(), sshProtocol.getUsername(), sshProtocol.getPassword(),
sshProtocol.getPrivateKey(), sshProtocol.getPrivateKeyPassphrase(), timeout, reuseConnection
);
}
}
@@ -20,6 +20,7 @@ package org.apache.hertzbeat.collector.collect.database;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
import org.apache.hertzbeat.common.entity.job.Metrics;
@@ -99,28 +100,27 @@ class JdbcCommonCollectTest {
"dm"
};
for (String platform : platforms) {
assertDoesNotThrow(() -> {
JdbcProtocol jdbc = new JdbcProtocol();
jdbc.setPlatform(platform);
Metrics metrics = new Metrics();
metrics.setJdbc(jdbc);
CollectRep.MetricsData.Builder builder = CollectRep.MetricsData.newBuilder();
jdbcCommonCollect.collect(builder, metrics);
});
}
// invalid platform
assertThrows(IllegalArgumentException.class, () -> {
JdbcProtocol jdbc = new JdbcProtocol();
jdbc.setPlatform("invalid");
jdbc.setPlatform(platform);
Metrics metrics = new Metrics();
metrics.setJdbc(jdbc);
CollectRep.MetricsData.Builder builder = CollectRep.MetricsData.newBuilder();
jdbcCommonCollect.collect(builder, metrics);
});
assertNotEquals(builder.getMsg(), "Query Error: Not support database platform: " + platform);
}
// invalid platform
JdbcProtocol jdbc = new JdbcProtocol();
jdbc.setPlatform("invalid");
Metrics metrics = new Metrics();
metrics.setJdbc(jdbc);
CollectRep.MetricsData.Builder builder = CollectRep.MetricsData.newBuilder();
jdbcCommonCollect.collect(builder, metrics);
assertEquals(builder.getCode(), CollectRep.Code.FAIL);
assertEquals(builder.getMsg(), "Query Error: Not support database platform: invalid");
}
@Test
@@ -25,6 +25,7 @@ import org.apache.sshd.common.NamedFactory;
import org.apache.sshd.common.PropertyResolverUtils;
import org.apache.sshd.common.kex.BuiltinDHFactories;
import org.apache.sshd.core.CoreModuleProperties;
import org.apache.sshd.server.forward.AcceptAllForwardingFilter;
/**
* common ssh pool client
@@ -52,6 +53,7 @@ public class CommonSshClient {
BuiltinDHFactories.VALUES,
ClientBuilder.DH2KEX
));
SSH_CLIENT.setForwardingFilter(new AcceptAllForwardingFilter());
// todo when connect AlibabaCloud ubuntu server, custom signature factories will cause error, why?
// SSH_CLIENT.setSignatureFactories(new ArrayList<>(BuiltinSignatures.VALUES));
SSH_CLIENT.start();
@@ -0,0 +1,104 @@
/*
* 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.collector.collect.common.ssh;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.collector.collect.common.cache.AbstractConnection;
import org.apache.hertzbeat.collector.collect.common.cache.CacheIdentifier;
import org.apache.hertzbeat.collector.collect.common.cache.GlobalConnectionCache;
import org.apache.hertzbeat.collector.collect.common.cache.SshConnect;
import org.apache.hertzbeat.collector.util.PrivateKeyUtils;
import org.apache.sshd.client.SshClient;
import org.apache.sshd.client.session.ClientSession;
import org.apache.sshd.common.config.keys.FilePasswordProvider;
import org.apache.sshd.common.util.security.SecurityUtils;
import org.springframework.util.StringUtils;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
/**
* ssh helper
*/
@Slf4j
public class SshHelper {
private static final GlobalConnectionCache CONNECTION_COMMON_CACHE = GlobalConnectionCache.getInstance();
public static ClientSession getConnectSession(String host, String port, String username, String password, String privateKey,
String privateKeyPassphrase, int timeout, boolean reuseConnection)
throws IOException, GeneralSecurityException {
CacheIdentifier identifier = CacheIdentifier.builder()
.ip(host).port(port)
.username(username).password(password)
.build();
ClientSession clientSession = null;
if (reuseConnection) {
Optional<AbstractConnection<?>> cacheOption = CONNECTION_COMMON_CACHE.getCache(identifier, true);
if (cacheOption.isPresent()) {
SshConnect sshConnect = (SshConnect) cacheOption.get();
clientSession = sshConnect.getConnection();
try {
if (clientSession == null || clientSession.isClosed() || clientSession.isClosing()) {
clientSession = null;
CONNECTION_COMMON_CACHE.removeCache(identifier);
}
} catch (Exception e) {
log.warn(e.getMessage());
clientSession = null;
CONNECTION_COMMON_CACHE.removeCache(identifier);
}
}
if (clientSession != null) {
return clientSession;
}
}
SshClient sshClient = CommonSshClient.getSshClient();
clientSession = sshClient.connect(username, host, Integer.parseInt(port))
.verify(timeout, TimeUnit.MILLISECONDS).getSession();
if (StringUtils.hasText(password)) {
clientSession.addPasswordIdentity(password);
} else if (StringUtils.hasText(privateKey)) {
var resourceKey = PrivateKeyUtils.writePrivateKey(host, privateKey);
FilePasswordProvider passwordProvider = (session, resource, index) -> {
if (StringUtils.hasText(privateKeyPassphrase)) {
return privateKeyPassphrase;
}
return null;
};
SecurityUtils.loadKeyPairIdentities(null, () -> resourceKey, new FileInputStream(resourceKey), passwordProvider)
.forEach(clientSession::addPublicKeyIdentity);
} // else auth with localhost private public key certificates
// auth
if (!clientSession.auth().verify(timeout, TimeUnit.MILLISECONDS).isSuccess()) {
clientSession.close();
throw new IllegalArgumentException("ssh auth failed.");
}
if (reuseConnection) {
SshConnect sshConnect = new SshConnect(clientSession);
CONNECTION_COMMON_CACHE.addCache(identifier, sshConnect);
}
return clientSession;
}
}
@@ -0,0 +1,313 @@
/*
* 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.collector.collect.common.ssh;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.RemovalCause;
import com.github.benmanes.caffeine.cache.Scheduler;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.common.entity.job.SshTunnel;
import org.apache.sshd.client.session.ClientSession;
import org.apache.sshd.client.session.forward.ExplicitPortForwardingTracker;
import org.apache.sshd.common.util.net.SshdSocketAddress;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.io.IOException;
import java.net.ServerSocket;
import java.security.GeneralSecurityException;
import java.time.Duration;
import java.util.Comparator;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Predicate;
/**
* Ssh Tunnel Helper
*/
@Slf4j
public class SshTunnelHelper {
private static final long DEFAULT_CACHE_TIMEOUT = 500 * 1000;
private static final Cache<SshClientSessionWrapper, LocalPortForwardingWrapper> TRACKER_CACHE =
Caffeine.newBuilder()
.initialCapacity(1)
.maximumSize(1000)
.expireAfterAccess(Duration.ofMillis(DEFAULT_CACHE_TIMEOUT))
.scheduler(Scheduler.systemScheduler())
.removalListener((key, value, cause) -> {
if (cause == RemovalCause.REPLACED) {
return;
}
if (key != null && value != null) {
// 1. try close tunnel
SshClientSessionWrapper clientSessionWrapper = (SshClientSessionWrapper) key;
LocalPortForwardingWrapper wrapper = (LocalPortForwardingWrapper) value;
wrapper.remove(clientSessionWrapper.getClientSession());
// 2. try close session
if (!clientSessionWrapper.isShareConnection()) {
try {
clientSessionWrapper.close();
log.info("[SSH Tunnel] close unshared ssh connection, {}", clientSessionWrapper);
} catch (IOException e) {
log.error("[SSH Tunnel] close unshared ssh connection error", e);
}
}
}
})
.build();
/**
* check ssh tunnel param
*
* @param sshTunnel ssh tunnel param
*/
public static void checkTunnelParam(SshTunnel sshTunnel) {
if (sshTunnel == null || !Boolean.parseBoolean(sshTunnel.getEnable())) {
return;
}
if (!StringUtils.hasText(sshTunnel.getHost())) {
throw new IllegalArgumentException("ssh tunnel must has ssh host param");
}
if (!StringUtils.hasText(sshTunnel.getPort())) {
throw new IllegalArgumentException("ssh tunnel must has ssh port param");
}
if (!StringUtils.hasText(sshTunnel.getUsername())) {
throw new IllegalArgumentException("ssh tunnel must has ssh username param");
}
}
/**
* create ssh tunnel
*
* @param sshTunnel ssh tunnel param
* @param remoteHost remote host
* @param remotePort remote port
* @return local port
*/
public static int localPortForward(SshTunnel sshTunnel, String remoteHost, String remotePort) throws GeneralSecurityException, IOException {
boolean shareConnection = Boolean.parseBoolean(sshTunnel.getShareConnection());
// 1. get ssh session
ClientSession session = SshHelper.getConnectSession(sshTunnel.getHost(), sshTunnel.getPort(),
sshTunnel.getUsername(), sshTunnel.getPassword(), sshTunnel.getPrivateKey(), sshTunnel.getPrivateKeyPassphrase(),
Integer.parseInt(sshTunnel.getTimeout()), shareConnection);
SshClientSessionWrapper sessionWrapper = new SshClientSessionWrapper(session, shareConnection);
// 2. get tunnel
LocalPortForwardingWrapper forwardingWrapper = selectWrapper(
TRACKER_CACHE.getIfPresent(sessionWrapper), sessionWrapper, remoteHost, remotePort);
int localPort;
if (forwardingWrapper == null) {
localPort = getRandomPort();
LocalPortForwardingWrapper newForwardingWrapper = sessionWrapper
.createLocalPortForwardingTracker(localPort, remoteHost, Integer.parseInt(remotePort));
if (TRACKER_CACHE.getIfPresent(sessionWrapper) == null) {
TRACKER_CACHE.put(sessionWrapper, newForwardingWrapper);
}
log.info("[SSH Tunnel] created ssh forwarding tracker ssh:{}, remote:{}, localPort:{}",
sshTunnel.getHost() + ":" + sshTunnel.getPort(), remoteHost + ":" + remotePort, localPort);
} else {
localPort = forwardingWrapper.getTracker().getLocalAddress().getPort();
}
return localPort;
}
/**
* get tunnel
*
* @param wrapper LocalPortForwardingWrapper
* @param sessionWrapper SshClientSessionWrapper
* @param remoteHost remote host
* @param remotePort remote port
* @return LocalPortForwardingWrapper
*/
private static LocalPortForwardingWrapper selectWrapper(LocalPortForwardingWrapper wrapper, SshClientSessionWrapper sessionWrapper,
String remoteHost, String remotePort) {
if (wrapper == null) {
return null;
}
List<LocalPortForwardingWrapper> selectList = wrapper.select(sessionWrapper.getClientSession(), localPortForwardWrapper -> {
if (!localPortForwardWrapper.isOpen()) {
return false;
}
ExplicitPortForwardingTracker tracker = localPortForwardWrapper.getTracker();
SshdSocketAddress remoteAddress = tracker.getRemoteAddress();
return Objects.equals(remoteAddress.getHostName(), remoteHost)
&& Objects.equals(remoteAddress.getPort(), Integer.parseInt(remotePort));
});
if (selectList.isEmpty()) {
return null;
}
LocalPortForwardingWrapper selected;
if (selectList.size() == 1) {
selected = selectList.get(0);
} else {
selected = selectList.stream().min(Comparator.comparing(LocalPortForwardingWrapper::getLastAccessTime)).get();
}
selected.setLastAccessTime(System.currentTimeMillis());
return selected;
}
private static int getRandomPort() throws IOException {
try (ServerSocket serverSocket = new ServerSocket(0)) {
return serverSocket.getLocalPort();
}
}
@Getter
@Setter
@EqualsAndHashCode
private static class SshClientSessionWrapper {
private ClientSession clientSession;
private boolean shareConnection;
public SshClientSessionWrapper(ClientSession clientSession, boolean shareConnection) {
this.clientSession = clientSession;
this.shareConnection = shareConnection;
}
/**
* Starts a local port forwarding
* @param localPort local port
* @param remoteHost remove host
* @param remotePort remote port
* @return LocalPortForwardingWrapper
*/
public LocalPortForwardingWrapper createLocalPortForwardingTracker(Integer localPort, String remoteHost, Integer remotePort) throws IOException {
SshdSocketAddress remoteAddress = new SshdSocketAddress(remoteHost, remotePort);
SshdSocketAddress localAddress = new SshdSocketAddress("localhost", localPort);
ExplicitPortForwardingTracker tracker = clientSession.createLocalPortForwardingTracker(localAddress, remoteAddress);
return new LocalPortForwardingWrapper(tracker);
}
/**
* close client session
*/
public void close() throws IOException {
clientSession.close();
}
@Override
public String toString() {
return "{ ssh:%s, shareConnection:%b }".formatted(clientSession, shareConnection);
}
}
@Getter
@Setter
@EqualsAndHashCode
private static class LocalPortForwardingWrapper {
private static Map<ClientSession, List<LocalPortForwardingWrapper>> map = new ConcurrentHashMap<>();
private ExplicitPortForwardingTracker tracker;
private Long lastAccessTime;
public LocalPortForwardingWrapper(ExplicitPortForwardingTracker tracker) {
this.tracker = tracker;
this.lastAccessTime = System.currentTimeMillis();
map.computeIfAbsent(tracker.getClientSession(), (key) -> new ArrayList<>()).add(this);
}
/**
* select ClientSession LocalPortForwardingWrapper List
* @param session ssh client session
* @param predicate condition
* @return LocalPortForwardWrapper
*/
public List<LocalPortForwardingWrapper> select(ClientSession session, Predicate<LocalPortForwardingWrapper> predicate) {
List<LocalPortForwardingWrapper> trackerList = map.get(session);
if (CollectionUtils.isEmpty(trackerList)) {
return trackerList;
}
List<LocalPortForwardingWrapper> list = new ArrayList<>();
long currentTimeMillis = System.currentTimeMillis();
Iterator<LocalPortForwardingWrapper> iterator = trackerList.iterator();
while (iterator.hasNext()) {
LocalPortForwardingWrapper wrapper = iterator.next();
// lazy remove
if (currentTimeMillis - wrapper.getLastAccessTime() > DEFAULT_CACHE_TIMEOUT) {
try {
wrapper.getTracker().close();
iterator.remove();
log.info("[SSH Tunnel] Lazy Remove ssh local port forwarding {}", wrapper);
} catch (IOException e) {
log.warn("[SSH Tunnel] Lazy Remove ssh local port forwarding Error", e);
}
} else if (predicate == null || predicate.test(wrapper)) {
list.add(wrapper);
}
}
return list;
}
/**
* remove session local port forwarding
* @param session ssh client session
*/
public void remove(ClientSession session) {
List<LocalPortForwardingWrapper> trackerList = map.get(session);
if (CollectionUtils.isEmpty(trackerList)) {
return;
}
Iterator<LocalPortForwardingWrapper> iterator = trackerList.iterator();
while (iterator.hasNext()){
try {
LocalPortForwardingWrapper next = iterator.next();
next.close();
iterator.remove();
log.info("[SSH Tunnel] Remove ssh local port forwarding, {}", next);
} catch (IOException e) {
log.error("[SSH Tunnel] Remove ssh session local port forwarding error", e);
}
}
}
public void close() throws IOException {
tracker.close();
}
public boolean isOpen() {
return tracker.isOpen();
}
@Override
public String toString() {
return "{ ssh:%s, remote:%s, localPort:%d }".formatted(
tracker.getSession().getConnectAddress(),
tracker.getRemoteAddress(),
tracker.getLocalAddress().getPort()
);
}
}
}
@@ -154,13 +154,11 @@ public class DispatchProperties {
/**
* Schedule Data Export Configuration Properties
* 调度数据出口配置属性
*/
public static class ExportProperties {
/**
* kafka configuration information
* kafka配置信息
*/
private KafkaProperties kafka;
@@ -178,18 +176,15 @@ public class DispatchProperties {
public static class KafkaProperties {
/**
* Whether the kafka data export is started
* kafka数据出口是否启动
*/
private boolean enabled = true;
/**
* kafka's connection server url
* kafka的连接服务器url
*/
private String servers = "http://127.0.0.1:2379";
/**
* Topic name to send data to
* 发送数据的topic名称
*/
private String topic;
@@ -223,7 +223,9 @@ public class KafkaCollectImpl extends AbstractCollect {
@Override
public void preCheck(Metrics metrics) throws IllegalArgumentException {
Assert.isTrue(metrics != null, "Metrics cannot be null");
KafkaProtocol kafkaProtocol = metrics.getKclient();
// Ensure that metrics and kafkaProtocol are not null
Assert.isTrue(metrics != null && kafkaProtocol != null, "Kafka collect must have kafkaProtocol params");
// Ensure that host and port are not empty
@@ -42,7 +42,7 @@ public class KafkaCollectTest {
@Test
void preCheck() {
// metrics is null
assertThrows(NullPointerException.class, () -> {
assertThrows(IllegalArgumentException.class, () -> {
collect.preCheck(null);
});
@@ -51,22 +51,21 @@ public class KafkaCollectTest {
collect.preCheck(Metrics.builder().build());
});
KafkaProtocol kafka = new KafkaProtocol();
Metrics metric = Metrics.builder().kclient(kafka).build();
// kafka srv host is null
assertThrows(IllegalArgumentException.class, () -> {
KafkaProtocol kafka = new KafkaProtocol();
collect.preCheck(Metrics.builder().kclient(kafka).build());
collect.preCheck(metric);
});
// kafka port is null
assertThrows(IllegalArgumentException.class, () -> {
KafkaProtocol kafka = KafkaProtocol.builder().host("127.0.0.1").build();
collect.preCheck(Metrics.builder().kclient(kafka).build());
kafka.setHost("127.0.0.1");
collect.preCheck(metric);
});
// no exception throw
assertDoesNotThrow(() -> {
KafkaProtocol kafka = KafkaProtocol.builder().host("127.0.0.1").port("9092").build();
collect.preCheck(Metrics.builder().kclient(kafka).build());
kafka.setPort("9092");
collect.preCheck(metric);
});
}
+6
View File
@@ -173,6 +173,12 @@
<artifactId>snappy-java</artifactId>
<version>${snappy-java.version}</version>
</dependency>
<dependency>
<groupId>com.github.javaparser</groupId>
<artifactId>javaparser-core</artifactId>
<version>${javaparser.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
@@ -42,11 +42,6 @@ public class CommonProperties {
*/
private DataQueueProperties queue;
/**
* sms impl properties
*/
private SmsProperties sms;
/**
* data queue properties
*/
@@ -146,83 +141,4 @@ public class CommonProperties {
*/
private String alertsDataTopic;
}
/**
* sms properties
*/
@Getter
@Setter
public static class SmsProperties {
//Tencent cloud SMS configuration
private TencentSmsProperties tencent;
//Ali cloud SMS configuration
private AliYunSmsProperties aliYun;
}
/**
* tencent sms properties
*/
@Getter
@Setter
public static class TencentSmsProperties {
/**
* Tencent cloud account secret id
*/
private String secretId;
/**
* Tencent cloud account secret key
*/
private String secretKey;
/**
* SMS app id
*/
private String appId;
/**
* SMS signature
*/
private String signName;
/**
* SMS template ID
*/
private String templateId;
}
/**
* aliYun sms properties
*/
@Getter
@Setter
public static class AliYunSmsProperties {
/**
* Aliyun account access key id
*/
private String secretId;
/**
* Ali Cloud account access key
*/
private String secretKey;
/**
* SMS app id
*/
private String appId;
/**
* SMS signature
*/
private String signName;
/**
* ID of the SMS template
*/
private String templateId;
}
}
@@ -0,0 +1,32 @@
/*
* 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.constants;
/**
* SMS provider constants
*/
public interface SmsConstants {
// Tencent cloud SMS
String TENCENT = "tencent";
// Alibaba Cloud SMS
String ALIBABA = "alibaba";
// UniSMS
String UNISMS = "unisms";
}
@@ -41,7 +41,7 @@ public class Configmap implements Serializable {
private String key;
/**
* parameter value 参数value
* parameter value
*/
private Object value;
@@ -0,0 +1,80 @@
/*
* 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.entity.job;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.hertzbeat.common.entity.job.protocol.CommonRequestProtocol;
import org.apache.hertzbeat.common.entity.job.protocol.Protocol;
/**
* ssh tunnel
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class SshTunnel implements CommonRequestProtocol, Protocol {
/**
* enable ssh tunnel
*/
private String enable = "false";
/**
* IP ADDRESS OR DOMAIN NAME OF THE PEER HOST
*/
private String host;
/**
* Peer host port
*/
private String port = "22";
/**
* TIME OUT PERIOD
*/
private String timeout = "6000";
/**
* UserName
*/
private String username;
/**
* Password (optional)
*/
private String password;
/**
* Private key (optional)
*/
private String privateKey;
/**
* private key passphrase (optional)
*/
private String privateKeyPassphrase;
/**
* share connection session
*/
private String shareConnection = "true";
}
@@ -59,4 +59,10 @@ public class FtpProtocol implements CommonRequestProtocol, Protocol {
* Timeout
*/
private String timeout;
/**
* Whether ftp uses link encryption ssl/tls, i.e. ftp or sftp
*
*/
private String ssl = "false";
}
@@ -21,6 +21,7 @@ import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.hertzbeat.common.entity.job.SshTunnel;
/**
* Database configuration information implemented by the common jdbc specification
@@ -70,4 +71,9 @@ public class JdbcProtocol implements CommonRequestProtocol, Protocol {
* DATABASE LINK URL eg: jdbc:mysql://localhost:3306/usthe
*/
private String url;
/**
* ssh tunnel
*/
private SshTunnel sshTunnel;
}
@@ -21,6 +21,7 @@ import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.hertzbeat.common.entity.job.SshTunnel;
/**
* Redis Protocol
@@ -61,4 +62,9 @@ public class RedisProtocol implements CommonRequestProtocol, Protocol {
*/
private String timeout;
/**
* SSH TUNNEL
*/
private SshTunnel sshTunnel;
}
@@ -0,0 +1,30 @@
/*
* 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.support.event;
import org.springframework.context.ApplicationEvent;
/**
* the event for sms config change
*/
public class SmsConfigChangeEvent extends ApplicationEvent {
public SmsConfigChangeEvent(Object source) {
super(source);
}
}
@@ -142,7 +142,7 @@ public final class AesUtil {
/**
* Determine whether it is encrypted
* @param text text
* @return true-是 false-否
* @return true false
*/
public static boolean isCiphertext(String text, String decryptKey) {
// First use whether it is base64 to determine whether it has been encrypted
@@ -68,19 +68,19 @@ class KafkaCommonDataQueueTest {
when(commonProperties.getQueue()).thenReturn(dataQueueProperties);
when(dataQueueProperties.getKafka()).thenReturn(kafkaProperties);
// 设置所有必需的 topic
// Set all required topics
when(kafkaProperties.getMetricsDataTopic()).thenReturn("metricsDataTopic");
when(kafkaProperties.getAlertsDataTopic()).thenReturn("alertsDataTopic");
when(kafkaProperties.getMetricsDataToStorageTopic()).thenReturn("metricsDataToStorageTopic");
when(kafkaProperties.getServiceDiscoveryDataTopic()).thenReturn("serviceDiscoveryDataTopic");
when(kafkaProperties.getServers()).thenReturn("localhost:9092");
// 模拟 consumer 的 subscribe 方法
// Simulate the subscribe method for consumers
doNothing().when(metricsDataToAlertConsumer).subscribe(anyCollection());
kafkaCommonDataQueue = new KafkaCommonDataQueue(commonProperties);
// 使用反射设置私有字段
// Use reflection to set private fields
setPrivateField(kafkaCommonDataQueue, "metricsDataProducer", metricsDataProducer);
setPrivateField(kafkaCommonDataQueue, "metricsDataToAlertConsumer", metricsDataToAlertConsumer);
}
@@ -98,16 +98,16 @@ class KafkaCommonDataQueueTest {
@Test
void testPollMetricsDataToAlerter() throws InterruptedException {
// 创建一个测试数据
// Create a test data
CollectRep.MetricsData expectedData = CollectRep.MetricsData.newBuilder()
.setMetrics("test metrics")
.build();
// 创建一个包含测试数据的 ConsumerRecord
// Create a ConsumerRecord containing test data
ConsumerRecord<Long, CollectRep.MetricsData> record =
new ConsumerRecord<>("metricsDataTopic", 0, 0L, 1L, expectedData);
// 创建一个包含单个记录的 ConsumerRecords
// Create a ConsumerRecords containing a single record.
Map<TopicPartition, List<ConsumerRecord<Long, CollectRep.MetricsData>>> recordsMap =
Collections.singletonMap(
new TopicPartition("metricsDataTopic", 0),
@@ -0,0 +1,146 @@
/*
* 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 com.github.javaparser.JavaParser;
import com.github.javaparser.ParseResult;
import com.github.javaparser.ast.CompilationUnit;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.nio.file.FileVisitOption;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Stream;
/**
* Test case for checking Chinese characters in Java files
*/
@Slf4j
public class ChineseCharacterCheckTest {
private static final Pattern CHINESE_CHAR_PATTERN = Pattern.compile("[\u4e00-\u9fa5]");
private static final Set<String> EXCLUDED_FILES = new HashSet<>(Collections.singletonList("Metrics"));
private static final String MAIN_SOURCE_DIR = "src/main/java";
private static final String TEST_SOURCE_DIR = "src/test/java";
private final JavaParser javaParser = new JavaParser();
private final String sourceDir;
private final String testDir;
public ChineseCharacterCheckTest() {
boolean isWindowsOs = System.getProperty("os.name").toLowerCase().startsWith("win");
String separator = isWindowsOs ? "\\" : "/";
this.sourceDir = MAIN_SOURCE_DIR.replace("/", separator);
this.testDir = TEST_SOURCE_DIR.replace("/", separator);
}
@Test
void shouldNotContainChineseInComments() {
List<String> violations = scanForChineseCharacters(ScanTarget.COMMENTS);
assertNoChineseCharacters(violations);
}
private List<String> scanForChineseCharacters(ScanTarget target) {
List<String> violations = new ArrayList<>();
try (Stream<Path> paths = Files.walk(Paths.get(".."), FileVisitOption.FOLLOW_LINKS)) {
paths.filter(this::isValidJavaFile)
.forEach(path -> processFile(path, target, violations));
} catch (IOException e) {
throw new RuntimeException("Failed to scan Java files", e);
}
return violations;
}
private boolean isValidJavaFile(Path path) {
String pathStr = path.toString();
return pathStr.endsWith(".java")
&& (pathStr.contains(sourceDir) || pathStr.contains(testDir))
&& EXCLUDED_FILES.stream().noneMatch(pathStr::contains);
}
private void processFile(Path path, ScanTarget target, List<String> violations) {
try {
ParseResult<CompilationUnit> parseResult = javaParser.parse(Files.newInputStream(path));
parseResult.getResult().ifPresent(cu -> {
if (target.includeComments()) {
checkComments(cu, path, violations);
}
if (target.includeCode()) {
checkCode(cu, path, violations);
}
});
} catch (Exception e) {
log.error("Error processing file: {}", path, e);
}
}
private void checkComments(CompilationUnit cu, Path path, List<String> violations) {
cu.getAllContainedComments().stream()
.filter(comment -> CHINESE_CHAR_PATTERN.matcher(comment.getContent()).find())
.forEach(comment -> violations.add(formatViolation(path, "comment", comment.getContent().trim())));
}
private void checkCode(CompilationUnit cu, Path path, List<String> violations) {
cu.findAll(com.github.javaparser.ast.expr.StringLiteralExpr.class).stream()
.filter(str -> CHINESE_CHAR_PATTERN.matcher(str.getValue()).find())
.forEach(str -> violations.add(formatViolation(path, "code", str.getValue())));
}
private String formatViolation(Path path, String location, String content) {
return String.format("Chinese characters found in %s at %s: %s",
location, path.toAbsolutePath(), content);
}
private void assertNoChineseCharacters(List<String> violations) {
Assertions.assertEquals(0, violations.size(),
() -> String.format("Found Chinese characters in files:%n%s",
String.join(System.lineSeparator(), violations)));
}
private enum ScanTarget {
COMMENTS(true, false),
CODE(false, true),
ALL(true, true);
private final boolean checkComments;
private final boolean checkCode;
ScanTarget(boolean checkComments, boolean checkCode) {
this.checkComments = checkComments;
this.checkCode = checkCode;
}
public boolean includeComments() {
return checkComments;
}
public boolean includeCode() {
return checkCode;
}
}
}
@@ -53,5 +53,10 @@
<version>${hertzbeat.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
@@ -56,6 +56,7 @@ public class HttpMonitorE2eTest extends AbstractCollectE2eTest {
private static final int MOCK_SERVER_PORT = 52376;
private static final String LOCALHOST = "127.0.0.1";
private static final String RELATIVE_PATH = "/";
private static final List<String> ALLOW_EMPTY_WHITE_LIST = List.of("header");
private static HttpServer mockServer;
@AfterAll
@@ -96,7 +97,12 @@ public class HttpMonitorE2eTest extends AbstractCollectE2eTest {
List<Map<String, Configmap>> configmapFromPreCollectData = new LinkedList<>();
for (Metrics metricsDef : dockerJob.getMetrics()) {
metricsDef = CollectUtil.replaceCryPlaceholderToMetrics(metricsDef, configmapFromPreCollectData.size() > 0 ? configmapFromPreCollectData.get(0) : new HashMap<>());
CollectRep.MetricsData metricsData = validateMetricsCollection(metricsDef, metricsDef.getName());
CollectRep.MetricsData metricsData;
if (ALLOW_EMPTY_WHITE_LIST.contains(metricsDef.getName())) {
metricsData = validateMetricsCollection(metricsDef, metricsDef.getName(), true);
} else {
metricsData = validateMetricsCollection(metricsDef, metricsDef.getName());
}
configmapFromPreCollectData = CollectUtil.getConfigmapFromPreCollectData(metricsData);
}
}
@@ -38,6 +38,8 @@ import org.testcontainers.lifecycle.Startables;
import org.testcontainers.utility.DockerImageName;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
@@ -54,6 +56,7 @@ public class SshCollectE2eTest extends AbstractCollectE2eTest {
private static final String ROOT_USER = "root";
private static final int SSH_PORT = 22;
private static final int PASSWORD_LENGTH = 12;
private static final List<String> ALLOW_EMPTY_WHITE_LIST = Arrays.asList("top_mem_process", "top_cpu_process");
private static GenericContainer<?> linuxContainer;
@@ -88,8 +91,13 @@ public class SshCollectE2eTest extends AbstractCollectE2eTest {
Assertions.assertTrue(linuxContainer.isRunning(), "Ubuntu container should be running");
Job ubuntuJob = appService.getAppDefine("ubuntu");
ubuntuJob.getMetrics().forEach(metricsDef ->
validateMetricsCollection(metricsDef, metricsDef.getName()));
ubuntuJob.getMetrics().forEach(metricsDef -> {
if (ALLOW_EMPTY_WHITE_LIST.contains(metricsDef.getName())) {
validateMetricsCollection(metricsDef, metricsDef.getName(), true);
} else {
validateMetricsCollection(metricsDef, metricsDef.getName());
}
});
}
@Override
@@ -0,0 +1,128 @@
/*
* 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.collector.collect.basic.telnet;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.collector.collect.AbstractCollectE2eTest;
import org.apache.hertzbeat.collector.collect.telnet.TelnetCollectImpl;
import org.apache.hertzbeat.collector.util.CollectUtil;
import org.apache.hertzbeat.common.entity.job.Configmap;
import org.apache.hertzbeat.common.entity.job.Job;
import org.apache.hertzbeat.common.entity.job.Metrics;
import org.apache.hertzbeat.common.entity.job.protocol.Protocol;
import org.apache.hertzbeat.common.entity.job.protocol.TelnetProtocol;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.utility.DockerImageName;
import java.time.Duration;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
/**
* Integration test for Zookeeper monitoring functionality
*/
@Slf4j
@ExtendWith(MockitoExtension.class)
public class ZookeeperMonitorE2eTest extends AbstractCollectE2eTest {
private static final String ZOOKEEPER_IMAGE_NAME = "zookeeper:3.8.4";
private static final String ZOOKEEPER_NAME = "zookeeper";
private static final Integer ZOOKEEPER_PORT = 2181;
private static GenericContainer<?> zookeeperContainer;
@AfterAll
public static void tearDown() {
if (zookeeperContainer != null) {
zookeeperContainer.stop();
}
}
@BeforeEach
public void setUp() throws Exception {
super.setUp();
collect = new TelnetCollectImpl();
metrics = new Metrics();
try {
// Start Zookeeper container with custom configuration
zookeeperContainer = new GenericContainer<>(DockerImageName.parse(ZOOKEEPER_IMAGE_NAME))
.withExposedPorts(ZOOKEEPER_PORT)
.withEnv("ZOO_4LW_COMMANDS_WHITELIST", "*")
.withNetworkAliases(ZOOKEEPER_NAME)
.waitingFor(
Wait.forLogMessage(".*Started AdminServer on address.*\\n", 1)
.withStartupTimeout(Duration.ofSeconds(60))
)
.withLogConsumer(outputFrame -> {
log.info(outputFrame.getUtf8String());
});
zookeeperContainer.start();
log.info("Zookeeper container started at {}:{}",
zookeeperContainer.getHost(),
zookeeperContainer.getMappedPort(ZOOKEEPER_PORT));
} catch (Exception e) {
e.printStackTrace();
log.error("Failed to start Zookeeper container", e);
throw e;
}
Thread.sleep(30000);
}
@Override
protected CollectRep.MetricsData.Builder collectMetrics(Metrics metricsDef) {
TelnetProtocol telnetProtocol = (TelnetProtocol) buildProtocol(metricsDef);
metrics.setTelnet(telnetProtocol);
CollectRep.MetricsData.Builder metricsData = CollectRep.MetricsData.newBuilder();
metricsData.setApp(ZOOKEEPER_NAME);
metrics.setAliasFields(metricsDef.getAliasFields());
return collectMetricsData(metrics, metricsDef, metricsData);
}
@Override
protected Protocol buildProtocol(Metrics metricsDef) {
TelnetProtocol protocol = new TelnetProtocol();
protocol.setHost(zookeeperContainer.getHost());
protocol.setPort(String.valueOf(zookeeperContainer.getMappedPort(ZOOKEEPER_PORT)));
protocol.setCmd(metricsDef.getTelnet().getCmd());
return protocol;
}
@Test
public void testZookeeperMonitor() {
Assertions.assertTrue(zookeeperContainer.isRunning(), "Zookeeper container should be running");
Job dockerJob = appService.getAppDefine("zookeeper");
List<Map<String, Configmap>> configmapFromPreCollectData = new LinkedList<>();
for (Metrics metricsDef : dockerJob.getMetrics()) {
metricsDef = CollectUtil.replaceCryPlaceholderToMetrics(metricsDef, configmapFromPreCollectData.size() > 0 ? configmapFromPreCollectData.get(0) : new HashMap<>());
CollectRep.MetricsData metricsData = validateMetricsCollection(metricsDef, metricsDef.getName());
configmapFromPreCollectData = CollectUtil.getConfigmapFromPreCollectData(metricsData);
}
}
}
@@ -22,6 +22,7 @@ import org.apache.hertzbeat.collector.dispatch.CollectDataDispatch;
import org.apache.hertzbeat.collector.dispatch.MetricsCollect;
import org.apache.hertzbeat.collector.dispatch.timer.Timeout;
import org.apache.hertzbeat.collector.dispatch.timer.WheelTimerTask;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.job.Job;
import org.apache.hertzbeat.common.entity.job.Metrics;
import org.apache.hertzbeat.common.entity.job.protocol.Protocol;
@@ -77,20 +78,39 @@ public abstract class AbstractCollectE2eTest {
/**
* Validate metrics collection, check if the metrics values are not empty <br/>
* We believe that all monitoring metrics should have data
* @param metricsDef metrics definition
* @param metricName metric name
* @return metrics data
*/
protected CollectRep.MetricsData validateMetricsCollection(Metrics metricsDef, String metricName) {
// By default, we do not allow empty values
return validateMetricsCollection(metricsDef, metricName, false);
}
/**
* Validate metrics collection, check if the metrics values are not empty <br/>
* We believe that all monitoring metrics should have data
*
* @param metricsDef metrics definition
* @param metricName metric name
* @param allowEmpty In some special scenarios, it is not necessary to check if the value is `&nbsp;`
*/
protected CollectRep.MetricsData validateMetricsCollection(Metrics metricsDef, String metricName, boolean allowEmpty) {
CollectRep.MetricsData.Builder metricsData = collectMetrics(metricsDef);
metricsCollect.calculateFields(metricsDef, metricsData);
Assertions.assertTrue(metricsData.getValuesList().size() > 0,
String.format("%s metrics values should not be empty", metricName));
String.format("%s metrics values should not be empty, detail: %s", metricName, metricsData.getMsg()));
for (CollectRep.ValueRow valueRow : metricsData.getValuesList()) {
for (int i = 0; i < valueRow.getColumnsCount(); i++) {
Assertions.assertFalse(valueRow.getColumns(i).isEmpty(),
String.format("%s metric column %d should not be empty", metricName, i));
if (!allowEmpty) {
// Check if the value is not null
Assertions.assertNotEquals(CommonConstants.NULL_VALUE, valueRow.getColumns(i), String.format("%s metric column %d should not be null", metricName, i));
}
}
}
@@ -98,21 +118,31 @@ public abstract class AbstractCollectE2eTest {
return metricsData.build();
}
/**
* Set alias fields for metrics
*
* @param metrics metrics
* @param metricsDef metrics definition
*/
protected void setMetricsAliasFields(Metrics metrics, Metrics metricsDef) {
metrics.setAliasFields(metricsDef.getAliasFields() == null
? metricsDef.getFields().stream()
.map(Metrics.Field::getField)
.collect(Collectors.toList()) :
metricsDef.getAliasFields());
List<String> aliasFields = metricsDef.getAliasFields() == null
? metricsDef.getFields().stream().map(Metrics.Field::getField).collect(Collectors.toList())
: metricsDef.getAliasFields();
metrics.setAliasFields(aliasFields);
metricsDef.setAliasFields(aliasFields);
}
protected abstract CollectRep.MetricsData.Builder collectMetrics(Metrics metricsDef);
protected CollectRep.MetricsData.Builder collectMetricsData(Metrics metrics, Metrics metricsDef) {
CollectRep.MetricsData.Builder metricsData = CollectRep.MetricsData.newBuilder();
return this.collectMetricsData(metrics, metricsDef, metricsData);
}
protected CollectRep.MetricsData.Builder collectMetricsData(Metrics metrics, Metrics metricsDef, CollectRep.MetricsData.Builder metricsData) {
setMetricsAliasFields(metrics, metricsDef);
// Collect metrics
CollectRep.MetricsData.Builder metricsData = CollectRep.MetricsData.newBuilder();
collect.collect(metricsData, metrics);
return metricsData;
}
@@ -46,7 +46,6 @@ public class AppCount {
private String app;
/**
* 任务状态
* task status
*/
private transient byte status;
@@ -26,10 +26,15 @@ import lombok.Data;
@Data
public class SmsAlibabaConfig {
@NotBlank(message = "SecretId cannot be empty")
private String secretId;
@NotBlank(message = "AccessKeyId cannot be empty")
private String accessKeyId;
@NotBlank(message = "SecretKey cannot be empty")
private String secretKey;
@NotBlank(message = "AccessKeySecret cannot be empty")
private String accessKeySecret;
@NotBlank(message = "SignName cannot be empty")
private String signName;
@NotBlank(message = "TemplateCode cannot be null")
private String templateCode;
}
@@ -37,5 +37,7 @@ public class SmsNoticeSender {
private SmsAlibabaConfig alibaba;
private SmsUniSmsConfig unisms;
private boolean enable = true;
}
@@ -0,0 +1,62 @@
/*
* 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.manager.pojo.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.logging.log4j.core.config.plugins.validation.constraints.NotBlank;
/**
* UniSMS configuration
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SmsUniSmsConfig {
/**
* UniSMS access key id
*/
@NotBlank(message = "accessKeyId cannot be empty")
private String accessKeyId;
/**
* UniSMS access key secret, required for HMAC mode
*/
private String accessKeySecret;
/**
* SMS signature
*/
@NotBlank(message = "signature cannot be null")
private String signature;
/**
* SMS template ID
*/
@NotBlank(message = "templateId cannot be null")
private String templateId;
/**
* Authentication mode: simple or hmac, default is simple
*/
@NotBlank(message = "authMode cannot be null")
private String authMode = "simple";
}
@@ -436,11 +436,12 @@ public class MonitorServiceImpl implements MonitorService {
newJobId = collectJobScheduling.updateAsyncCollectJob(appDefine, collector);
}
monitor.setJobId(newJobId);
}
try {
detectMonitor(monitor, params, collector);
} catch (Exception ignored) {
// execute only in non paused status
try {
detectMonitor(monitor, params, collector);
} catch (Exception ignored) {
}
}
// After the update is successfully released, refresh the database
@@ -567,10 +568,12 @@ public class MonitorServiceImpl implements MonitorService {
if (StringUtils.isNotBlank(search)) {
Predicate predicateHost = criteriaBuilder.like(root.get("host"), "%" + search + "%");
Predicate predicateName = criteriaBuilder.like(criteriaBuilder.lower(root.get("name")), "%" + search.toLowerCase() + "%");
Predicate predicateId = criteriaBuilder.like(root.get("id"), "%" + search + "%");
if (StringUtils.isNumeric(search)){
Predicate predicateId = criteriaBuilder.equal(root.get("id"), Long.parseLong(search));
orList.add(predicateId);
}
orList.add(predicateHost);
orList.add(predicateName);
orList.add(predicateId);
}
if (StringUtils.isNotBlank(labels)) {
String[] labelAres = labels.split(",");
@@ -20,9 +20,13 @@ package org.apache.hertzbeat.manager.service.impl;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.lang.reflect.Type;
import jakarta.annotation.Resource;
import org.apache.hertzbeat.common.constants.GeneralConfigTypeEnum;
import org.apache.hertzbeat.base.dao.GeneralConfigDao;
import org.apache.hertzbeat.common.support.event.SmsConfigChangeEvent;
import org.apache.hertzbeat.manager.pojo.dto.SmsNoticeSender;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Service;
/**
@@ -32,6 +36,8 @@ import org.springframework.stereotype.Service;
@Service
public class SmsGeneralConfigServiceImpl extends AbstractGeneralConfigServiceImpl<SmsNoticeSender> {
@Resource
private ApplicationContext applicationContext;
/**
* SmsGeneralConfigServiceImpl's constructor creates an instance of this class
@@ -44,6 +50,14 @@ public class SmsGeneralConfigServiceImpl extends AbstractGeneralConfigServiceImp
public SmsGeneralConfigServiceImpl(GeneralConfigDao generalConfigDao, ObjectMapper objectMapper) {
super(generalConfigDao, objectMapper);
}
/**
* This method is used to handle the sms configuration change event.
*/
@Override
public void handler(SmsNoticeSender smsNoticeSender) {
applicationContext.publishEvent(new SmsConfigChangeEvent(applicationContext));
}
@Override
public String type() {
@@ -203,6 +203,28 @@ alerter:
# alert inhibit ttl unit ms, default 14400000(4 hours)
inhibit:
ttl: 14400000
sms:
enable: false
type: tencent
tencent:
secret-id:
secret-key:
app-id:
sign-name:
template-id:
alibaba:
access-key-id:
access-key-secret:
sign-name:
template-code:
unisms:
# auth-mode: simple or hmac
auth-mode: simple
access-key-id: YOUR_ACCESS_KEY_ID
# hmac mode need to fill in access-key-secret
access-key-secret: YOUR_ACCESS_KEY_SECRET
signature: YOUR_SMS_SIGNATURE
template-id: YOUR_TEMPLATE_ID
scheduler:
server:
@@ -88,6 +88,15 @@ params:
range: '[0,100000]'
required: true
defaultValue: 1000
- field: ssl
# name-param field display i18n name
name:
zh-CN: 启用SFTP
en-US: SFTP
# type-param field type(most mapping the html input type)
type: boolean
# required-true or false
required: true
# collect metrics config list
metrics:
# metrics - basic
@@ -122,3 +131,4 @@ metrics:
password: ^_^password^_^
direction: ^_^direction^_^
timeout: ^_^timeout^_^
ssl: ^_^ssl^_^
@@ -115,6 +115,78 @@ params:
required: false
# hide param-true or false
hide: true
- field: enableSshTunnel
name:
zh-CN: 是否启用SSH隧道
en-US: Enable SSH Tunnel
type: boolean
required: true
hide: true
- field: sshHost
name:
zh-CN: SSH Host
en-US: SSH Host
type: text
required: false
placeholder: 'When Enable SSH Tunnel'
hide: true
- field: sshPort
name:
zh-CN: SSH端口
en-US: SSH Port
type: number
range: '[0,65535]'
required: false
defaultValue: 22
placeholder: 'When Enable SSH tunnel'
hide: true
- field: sshTimeout
name:
zh-CN: SSH超时时间(ms)
en-US: SSH Timeout(ms)
type: number
required: false
range: '[400,200000]'
defaultValue: 6000
hide: true
- field: sshUsername
name:
zh-CN: SSH用户名
en-US: SSH Username
type: text
required: false
placeholder: 'When Enable SSH tunnel'
hide: true
- field: sshPassword
name:
zh-CN: SSH密码
en-US: SSH Password
type: password
required: false
hide: true
- field: sshShareConnection
name:
zh-CN: 是否共享SSH连接
en-US: Share SSH Connection
type: boolean
required: true
defaultValue: true
hide: true
- field: sshPrivateKey
name:
zh-CN: SSH私钥
en-US: SSH PrivateKey
type: textarea
placeholder: -----BEGIN RSA PRIVATE KEY-----
required: false
hide: true
- field: sshPrivateKeyPassphrase
name:
zh-CN: SSH密钥短语
en-US: SSH PrivateKey PassPhrase
type: password
required: false
hide: true
# collect metrics config list
metrics:
@@ -205,6 +277,16 @@ metrics:
sql: show global variables where Variable_name like 'version%' or Variable_name = 'max_connections' or Variable_name = 'datadir' or Variable_name = 'port' or Variable_name = 'thread_cache_size' or Variable_name = 'table_open_cache' or Variable_name = 'innodb_buffer_pool_size';
# JDBC url
url: ^_^url^_^
sshTunnel:
enable: ^_^enableSshTunnel^_^
host: ^_^sshHost^_^
port: ^_^sshPort^_^
timeout: ^_^sshTimeout^_^
username: ^_^sshUsername^_^
password: ^_^sshPassword^_^
privateKey: ^_^sshPrivateKey^_^
privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^
shareConnection: ^_^sshShareConnection^_^
- name: cache
# metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel
@@ -284,6 +366,16 @@ metrics:
# sql
sql: show global status like 'QCache%';
url: ^_^url^_^
sshTunnel:
enable: ^_^enableSshTunnel^_^
host: ^_^sshHost^_^
port: ^_^sshPort^_^
timeout: ^_^sshTimeout^_^
username: ^_^sshUsername^_^
password: ^_^sshPassword^_^
privateKey: ^_^sshPrivateKey^_^
privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^
shareConnection: ^_^sshShareConnection^_^
- name: performance
priority: 2
@@ -318,6 +410,16 @@ metrics:
queryType: columns
sql: show global status where Variable_name = 'questions' or Variable_name = 'uptime';
url: ^_^url^_^
sshTunnel:
enable: ^_^enableSshTunnel^_^
host: ^_^sshHost^_^
port: ^_^sshPort^_^
timeout: ^_^sshTimeout^_^
username: ^_^sshUsername^_^
password: ^_^sshPassword^_^
privateKey: ^_^sshPrivateKey^_^
privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^
shareConnection: ^_^sshShareConnection^_^
- name: innodb
priority: 3
@@ -384,6 +486,16 @@ metrics:
queryType: columns
sql: show global status where Variable_name like 'innodb%';
url: ^_^url^_^
sshTunnel:
enable: ^_^enableSshTunnel^_^
host: ^_^sshHost^_^
port: ^_^sshPort^_^
timeout: ^_^sshTimeout^_^
username: ^_^sshUsername^_^
password: ^_^sshPassword^_^
privateKey: ^_^sshPrivateKey^_^
privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^
shareConnection: ^_^sshShareConnection^_^
- name: status
priority: 4
@@ -479,6 +591,16 @@ metrics:
queryType: columns
sql: show global status where Variable_name like 'thread%' or Variable_name = 'com_select' or Variable_name = 'com_insert' or Variable_name = 'com_update' or Variable_name = 'com_delete' or Variable_name = 'com_commit' or Variable_name = 'com_rollback' or Variable_name = 'questions' or Variable_name = 'uptime';
url: ^_^url^_^
sshTunnel:
enable: ^_^enableSshTunnel^_^
host: ^_^sshHost^_^
port: ^_^sshPort^_^
timeout: ^_^sshTimeout^_^
username: ^_^sshUsername^_^
password: ^_^sshPassword^_^
privateKey: ^_^sshPrivateKey^_^
privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^
shareConnection: ^_^sshShareConnection^_^
- name: handler
priority: 5
@@ -558,6 +680,16 @@ metrics:
queryType: columns
sql: show global status like 'Handler%';
url: ^_^url^_^
sshTunnel:
enable: ^_^enableSshTunnel^_^
host: ^_^sshHost^_^
port: ^_^sshPort^_^
timeout: ^_^sshTimeout^_^
username: ^_^sshUsername^_^
password: ^_^sshPassword^_^
privateKey: ^_^sshPrivateKey^_^
privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^
shareConnection: ^_^sshShareConnection^_^
- name: connection
priority: 6
@@ -597,6 +729,16 @@ metrics:
queryType: columns
sql: show global status;
url: ^_^url^_^
sshTunnel:
enable: ^_^enableSshTunnel^_^
host: ^_^sshHost^_^
port: ^_^sshPort^_^
timeout: ^_^sshTimeout^_^
username: ^_^sshUsername^_^
password: ^_^sshPassword^_^
privateKey: ^_^sshPrivateKey^_^
privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^
shareConnection: ^_^sshShareConnection^_^
- name: thread
priority: 7
@@ -636,6 +778,16 @@ metrics:
queryType: columns
sql: show global status like 'thread%';
url: ^_^url^_^
sshTunnel:
enable: ^_^enableSshTunnel^_^
host: ^_^sshHost^_^
port: ^_^sshPort^_^
timeout: ^_^sshTimeout^_^
username: ^_^sshUsername^_^
password: ^_^sshPassword^_^
privateKey: ^_^sshPrivateKey^_^
privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^
shareConnection: ^_^sshShareConnection^_^
- name: tmp
priority: 8
@@ -670,6 +822,16 @@ metrics:
queryType: columns
sql: show global status where Variable_name like '%tmp%';
url: ^_^url^_^
sshTunnel:
enable: ^_^enableSshTunnel^_^
host: ^_^sshHost^_^
port: ^_^sshPort^_^
timeout: ^_^sshTimeout^_^
username: ^_^sshUsername^_^
password: ^_^sshPassword^_^
privateKey: ^_^sshPrivateKey^_^
privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^
shareConnection: ^_^sshShareConnection^_^
- name: select_type
priority: 9
@@ -714,6 +876,16 @@ metrics:
queryType: columns
sql: show global status where Variable_name like 'select%';
url: ^_^url^_^
sshTunnel:
enable: ^_^enableSshTunnel^_^
host: ^_^sshHost^_^
port: ^_^sshPort^_^
timeout: ^_^sshTimeout^_^
username: ^_^sshUsername^_^
password: ^_^sshPassword^_^
privateKey: ^_^sshPrivateKey^_^
privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^
shareConnection: ^_^sshShareConnection^_^
- name: sort
priority: 10
@@ -753,6 +925,16 @@ metrics:
queryType: columns
sql: show global status where Variable_name like 'sort%';
url: ^_^url^_^
sshTunnel:
enable: ^_^enableSshTunnel^_^
host: ^_^sshHost^_^
port: ^_^sshPort^_^
timeout: ^_^sshTimeout^_^
username: ^_^sshUsername^_^
password: ^_^sshPassword^_^
privateKey: ^_^sshPrivateKey^_^
privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^
shareConnection: ^_^sshShareConnection^_^
- name: table_lock
priority: 11
@@ -782,6 +964,16 @@ metrics:
queryType: columns
sql: show global status where Variable_name like 'table_lock%';
url: ^_^url^_^
sshTunnel:
enable: ^_^enableSshTunnel^_^
host: ^_^sshHost^_^
port: ^_^sshPort^_^
timeout: ^_^sshTimeout^_^
username: ^_^sshUsername^_^
password: ^_^sshPassword^_^
privateKey: ^_^sshPrivateKey^_^
privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^
shareConnection: ^_^sshShareConnection^_^
- name: process_state
priority: 12
@@ -812,6 +1004,16 @@ metrics:
queryType: multiRow
sql: select state, count(*) as num from information_schema.PROCESSLIST where state != '' group by state;
url: ^_^url^_^
sshTunnel:
enable: ^_^enableSshTunnel^_^
host: ^_^sshHost^_^
port: ^_^sshPort^_^
timeout: ^_^sshTimeout^_^
username: ^_^sshUsername^_^
password: ^_^sshPassword^_^
privateKey: ^_^sshPrivateKey^_^
privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^
shareConnection: ^_^sshShareConnection^_^
- name: slow_sql
priority: 13
@@ -864,3 +1066,13 @@ metrics:
queryType: multiRow
sql: select sql_text, start_time, db, query_time from mysql.slow_log;
url: ^_^url^_^
sshTunnel:
enable: ^_^enableSshTunnel^_^
host: ^_^sshHost^_^
port: ^_^sshPort^_^
timeout: ^_^sshTimeout^_^
username: ^_^sshUsername^_^
password: ^_^sshPassword^_^
privateKey: ^_^sshPrivateKey^_^
privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^
shareConnection: ^_^sshShareConnection^_^
@@ -91,6 +91,78 @@ params:
type: text
required: false
hide: true
- field: enableSshTunnel
name:
zh-CN: 是否启用SSH隧道
en-US: Enable SSH Tunnel
type: boolean
required: true
hide: true
- field: sshHost
name:
zh-CN: SSH Host
en-US: SSH Host
type: text
required: false
placeholder: 'When Enable SSH Tunnel'
hide: true
- field: sshPort
name:
zh-CN: SSH端口
en-US: SSH Port
type: number
range: '[0,65535]'
required: false
defaultValue: 22
placeholder: 'When Enable SSH tunnel'
hide: true
- field: sshTimeout
name:
zh-CN: SSH超时时间(ms)
en-US: SSH Timeout(ms)
type: number
required: false
range: '[400,200000]'
defaultValue: 6000
hide: true
- field: sshUsername
name:
zh-CN: SSH用户名
en-US: SSH Username
type: text
required: false
placeholder: 'When Enable SSH tunnel'
hide: true
- field: sshPassword
name:
zh-CN: SSH密码
en-US: SSH Password
type: password
required: false
hide: true
- field: sshShareConnection
name:
zh-CN: 是否共享SSH连接
en-US: Share SSH Connection
type: boolean
required: true
defaultValue: true
hide: true
- field: sshPrivateKey
name:
zh-CN: SSH私钥
en-US: SSH PrivateKey
type: textarea
placeholder: -----BEGIN RSA PRIVATE KEY-----
required: false
hide: true
- field: sshPrivateKeyPassphrase
name:
zh-CN: SSH密钥短语
en-US: SSH PrivateKey PassPhrase
type: password
required: false
hide: true
# collect metrics config list
metrics:
@@ -150,6 +222,16 @@ metrics:
sql: select name, setting as value from pg_settings where name = 'max_connections' or name = 'server_version' or name = 'server_encoding' or name = 'port' or name = 'data_directory';
# JDBC url
url: ^_^url^_^
sshTunnel:
enable: ^_^enableSshTunnel^_^
host: ^_^sshHost^_^
port: ^_^sshPort^_^
timeout: ^_^sshTimeout^_^
username: ^_^sshUsername^_^
password: ^_^sshPassword^_^
privateKey: ^_^sshPrivateKey^_^
privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^
shareConnection: ^_^sshShareConnection^_^
- name: state
i18n:
@@ -216,6 +298,16 @@ metrics:
queryType: multiRow
sql: SELECT COALESCE(datname,'shared-object') as db_name, conflicts, deadlocks, blks_read, blks_hit, blk_read_time, blk_write_time, stats_reset from pg_stat_database where (datname != 'template1' and datname != 'template0') or datname is null;
url: ^_^url^_^
sshTunnel:
enable: ^_^enableSshTunnel^_^
host: ^_^sshHost^_^
port: ^_^sshPort^_^
timeout: ^_^sshTimeout^_^
username: ^_^sshUsername^_^
password: ^_^sshPassword^_^
privateKey: ^_^sshPrivateKey^_^
privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^
shareConnection: ^_^sshShareConnection^_^
- name: activity
i18n:
@@ -241,6 +333,16 @@ metrics:
queryType: oneRow
sql: SELECT count(*) as running FROM pg_stat_activity WHERE NOT pid=pg_backend_pid();
url: ^_^url^_^
sshTunnel:
enable: ^_^enableSshTunnel^_^
host: ^_^sshHost^_^
port: ^_^sshPort^_^
timeout: ^_^sshTimeout^_^
username: ^_^sshUsername^_^
password: ^_^sshPassword^_^
privateKey: ^_^sshPrivateKey^_^
privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^
shareConnection: ^_^sshShareConnection^_^
- name: resource_config
i18n:
@@ -294,6 +396,16 @@ metrics:
queryType: columns
sql: show all;
url: ^_^url^_^
sshTunnel:
enable: ^_^enableSshTunnel^_^
host: ^_^sshHost^_^
port: ^_^sshPort^_^
timeout: ^_^sshTimeout^_^
username: ^_^sshUsername^_^
password: ^_^sshPassword^_^
privateKey: ^_^sshPrivateKey^_^
privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^
shareConnection: ^_^sshShareConnection^_^
- name: connection
i18n:
@@ -318,6 +430,16 @@ metrics:
queryType: oneRow
sql: select count(1) as active from pg_stat_activity;
url: ^_^url^_^
sshTunnel:
enable: ^_^enableSshTunnel^_^
host: ^_^sshHost^_^
port: ^_^sshPort^_^
timeout: ^_^sshTimeout^_^
username: ^_^sshUsername^_^
password: ^_^sshPassword^_^
privateKey: ^_^sshPrivateKey^_^
privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^
shareConnection: ^_^sshShareConnection^_^
- name: connection_state
i18n:
@@ -348,6 +470,16 @@ metrics:
queryType: multiRow
sql: select COALESCE(state, 'other') as state, count(*) as num from pg_stat_activity group by state;
url: ^_^url^_^
sshTunnel:
enable: ^_^enableSshTunnel^_^
host: ^_^sshHost^_^
port: ^_^sshPort^_^
timeout: ^_^sshTimeout^_^
username: ^_^sshUsername^_^
password: ^_^sshPassword^_^
privateKey: ^_^sshPrivateKey^_^
privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^
shareConnection: ^_^sshShareConnection^_^
- name: connection_db
i18n:
@@ -378,6 +510,16 @@ metrics:
queryType: multiRow
sql: select count(*) as active, COALESCE(datname, 'other') as db_name from pg_stat_activity group by datname;
url: ^_^url^_^
sshTunnel:
enable: ^_^enableSshTunnel^_^
host: ^_^sshHost^_^
port: ^_^sshPort^_^
timeout: ^_^sshTimeout^_^
username: ^_^sshUsername^_^
password: ^_^sshPassword^_^
privateKey: ^_^sshPrivateKey^_^
privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^
shareConnection: ^_^sshShareConnection^_^
- name: tuple
i18n:
@@ -422,6 +564,16 @@ metrics:
queryType: multiRow
sql: select sum(tup_fetched) as fetched, sum(tup_updated) as updated, sum(tup_deleted) as deleted, sum(tup_inserted) as inserted, sum(tup_returned) as returned from pg_stat_database;
url: ^_^url^_^
sshTunnel:
enable: ^_^enableSshTunnel^_^
host: ^_^sshHost^_^
port: ^_^sshPort^_^
timeout: ^_^sshTimeout^_^
username: ^_^sshUsername^_^
password: ^_^sshPassword^_^
privateKey: ^_^sshPrivateKey^_^
privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^
shareConnection: ^_^sshShareConnection^_^
- name: temp_file
i18n:
@@ -458,6 +610,16 @@ metrics:
queryType: multiRow
sql: select COALESCE(datname, 'other') as db_name, sum(temp_files) as num, sum(temp_bytes) as size from pg_stat_database group by datname;
url: ^_^url^_^
sshTunnel:
enable: ^_^enableSshTunnel^_^
host: ^_^sshHost^_^
port: ^_^sshPort^_^
timeout: ^_^sshTimeout^_^
username: ^_^sshUsername^_^
password: ^_^sshPassword^_^
privateKey: ^_^sshPrivateKey^_^
privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^
shareConnection: ^_^sshShareConnection^_^
- name: lock
i18n:
@@ -495,6 +657,16 @@ metrics:
queryType: multiRow
sql: SELECT COALESCE(datname,'shared-object') as db_name, conflicts, deadlocks from pg_stat_database where (datname != 'template1' and datname != 'template0') or datname is null;
url: ^_^url^_^
sshTunnel:
enable: ^_^enableSshTunnel^_^
host: ^_^sshHost^_^
port: ^_^sshPort^_^
timeout: ^_^sshTimeout^_^
username: ^_^sshUsername^_^
password: ^_^sshPassword^_^
privateKey: ^_^sshPrivateKey^_^
privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^
shareConnection: ^_^sshShareConnection^_^
- name: slow_sql
i18n:
@@ -552,6 +724,16 @@ metrics:
queryType: multiRow
sql: select * from pg_stat_statements;
url: ^_^url^_^
sshTunnel:
enable: ^_^enableSshTunnel^_^
host: ^_^sshHost^_^
port: ^_^sshPort^_^
timeout: ^_^sshTimeout^_^
username: ^_^sshUsername^_^
password: ^_^sshPassword^_^
privateKey: ^_^sshPrivateKey^_^
privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^
shareConnection: ^_^sshShareConnection^_^
- name: transaction
i18n:
@@ -589,6 +771,16 @@ metrics:
queryType: multiRow
sql: select COALESCE(datname, 'other') as db_name, sum(xact_commit) as commits, sum(xact_rollback) as rollbacks from pg_stat_database group by datname;
url: ^_^url^_^
sshTunnel:
enable: ^_^enableSshTunnel^_^
host: ^_^sshHost^_^
port: ^_^sshPort^_^
timeout: ^_^sshTimeout^_^
username: ^_^sshUsername^_^
password: ^_^sshPassword^_^
privateKey: ^_^sshPrivateKey^_^
privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^
shareConnection: ^_^sshShareConnection^_^
- name: conflicts
i18n:
@@ -639,6 +831,16 @@ metrics:
queryType: multiRow
sql: select datname as db_name, confl_tablespace as tablespace, confl_lock as lock, confl_snapshot as snapshot, confl_bufferpin as bufferpin, confl_deadlock as deadlock from pg_stat_database_conflicts;
url: ^_^url^_^
sshTunnel:
enable: ^_^enableSshTunnel^_^
host: ^_^sshHost^_^
port: ^_^sshPort^_^
timeout: ^_^sshTimeout^_^
username: ^_^sshUsername^_^
password: ^_^sshPassword^_^
privateKey: ^_^sshPrivateKey^_^
privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^
shareConnection: ^_^sshShareConnection^_^
- name: cache_hit_ratio
i18n:
@@ -676,6 +878,16 @@ metrics:
queryType: multiRow
sql: select datname as db_name, blks_hit, blks_read from pg_stat_database;
url: ^_^url^_^
sshTunnel:
enable: ^_^enableSshTunnel^_^
host: ^_^sshHost^_^
port: ^_^sshPort^_^
timeout: ^_^sshTimeout^_^
username: ^_^sshUsername^_^
password: ^_^sshPassword^_^
privateKey: ^_^sshPrivateKey^_^
privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^
shareConnection: ^_^sshShareConnection^_^
- name: checkpoint
i18n:
@@ -707,6 +919,16 @@ metrics:
queryType: oneRow
sql: select checkpoint_sync_time, checkpoint_write_time from pg_stat_bgwriter;
url: ^_^url^_^
sshTunnel:
enable: ^_^enableSshTunnel^_^
host: ^_^sshHost^_^
port: ^_^sshPort^_^
timeout: ^_^sshTimeout^_^
username: ^_^sshUsername^_^
password: ^_^sshPassword^_^
privateKey: ^_^sshPrivateKey^_^
privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^
shareConnection: ^_^sshShareConnection^_^
- name: buffer
i18n:
@@ -751,3 +973,13 @@ metrics:
queryType: oneRow
sql: select buffers_alloc as allocated, buffers_backend_fsync as fsync_calls_by_backend, buffers_backend as written_directly_by_backend, buffers_clean as written_by_background_writer, buffers_checkpoint as written_during_checkpoints from pg_stat_bgwriter;
url: ^_^url^_^
sshTunnel:
enable: ^_^enableSshTunnel^_^
host: ^_^sshHost^_^
port: ^_^sshPort^_^
timeout: ^_^sshTimeout^_^
username: ^_^sshUsername^_^
password: ^_^sshPassword^_^
privateKey: ^_^sshPrivateKey^_^
privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^
shareConnection: ^_^sshShareConnection^_^
@@ -84,7 +84,78 @@ params:
en-US: Password
type: password
required: false
- field: enableSshTunnel
name:
zh-CN: 是否启用SSH隧道
en-US: Enable SSH Tunnel
type: boolean
required: true
hide: true
- field: sshHost
name:
zh-CN: SSH Host
en-US: SSH Host
type: text
required: false
placeholder: 'When Enable SSH Tunnel'
hide: true
- field: sshPort
name:
zh-CN: SSH端口
en-US: SSH Port
type: number
range: '[0,65535]'
required: false
defaultValue: 22
placeholder: 'When Enable SSH tunnel'
hide: true
- field: sshTimeout
name:
zh-CN: SSH超时时间(ms)
en-US: SSH Timeout(ms)
type: number
required: false
range: '[400,200000]'
defaultValue: 6000
hide: true
- field: sshUsername
name:
zh-CN: SSH用户名
en-US: SSH Username
type: text
required: false
placeholder: 'When Enable SSH tunnel'
hide: true
- field: sshPassword
name:
zh-CN: SSH密码
en-US: SSH Password
type: password
required: false
hide: true
- field: sshShareConnection
name:
zh-CN: 是否共享SSH连接
en-US: Share SSH Connection
type: boolean
required: true
defaultValue: true
hide: true
- field: sshPrivateKey
name:
zh-CN: SSH私钥
en-US: SSH PrivateKey
type: textarea
placeholder: -----BEGIN RSA PRIVATE KEY-----
required: false
hide: true
- field: sshPrivateKeyPassphrase
name:
zh-CN: SSH密钥短语
en-US: SSH PrivateKey PassPhrase
type: password
required: false
hide: true
# collect metrics config list
metrics:
# metrics - server
@@ -231,6 +302,16 @@ metrics:
password: ^_^password^_^
# timeout unitms
timeout: ^_^timeout^_^
sshTunnel:
enable: ^_^enableSshTunnel^_^
host: ^_^sshHost^_^
port: ^_^sshPort^_^
timeout: ^_^sshTimeout^_^
username: ^_^sshUsername^_^
password: ^_^sshPassword^_^
privateKey: ^_^sshPrivateKey^_^
privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^
shareConnection: ^_^sshShareConnection^_^
# metrics - clients
- name: clients
# metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel
@@ -288,6 +369,16 @@ metrics:
username: ^_^username^_^
password: ^_^password^_^
timeout: ^_^timeout^_^
sshTunnel:
enable: ^_^enableSshTunnel^_^
host: ^_^sshHost^_^
port: ^_^sshPort^_^
timeout: ^_^sshTimeout^_^
username: ^_^sshUsername^_^
password: ^_^sshPassword^_^
privateKey: ^_^sshPrivateKey^_^
privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^
shareConnection: ^_^sshShareConnection^_^
# metrics - memory
- name: memory
# metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel
@@ -527,6 +618,16 @@ metrics:
password: ^_^password^_^
# timeout unitms
timeout: ^_^timeout^_^
sshTunnel:
enable: ^_^enableSshTunnel^_^
host: ^_^sshHost^_^
port: ^_^sshPort^_^
timeout: ^_^sshTimeout^_^
username: ^_^sshUsername^_^
password: ^_^sshPassword^_^
privateKey: ^_^sshPrivateKey^_^
privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^
shareConnection: ^_^sshShareConnection^_^
# metrics - persistence
- name: persistence
@@ -668,6 +769,16 @@ metrics:
password: ^_^password^_^
# timeout unitms
timeout: ^_^timeout^_^
sshTunnel:
enable: ^_^enableSshTunnel^_^
host: ^_^sshHost^_^
port: ^_^sshPort^_^
timeout: ^_^sshTimeout^_^
username: ^_^sshUsername^_^
password: ^_^sshPassword^_^
privateKey: ^_^sshPrivateKey^_^
privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^
shareConnection: ^_^sshShareConnection^_^
# metrics - stats
- name: stats
@@ -884,6 +995,16 @@ metrics:
password: ^_^password^_^
# timeout unitms
timeout: ^_^timeout^_^
sshTunnel:
enable: ^_^enableSshTunnel^_^
host: ^_^sshHost^_^
port: ^_^sshPort^_^
timeout: ^_^sshTimeout^_^
username: ^_^sshUsername^_^
password: ^_^sshPassword^_^
privateKey: ^_^sshPrivateKey^_^
privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^
shareConnection: ^_^sshShareConnection^_^
# metrics - replication
- name: replication
@@ -965,6 +1086,16 @@ metrics:
password: ^_^password^_^
# timeout unitms
timeout: ^_^timeout^_^
sshTunnel:
enable: ^_^enableSshTunnel^_^
host: ^_^sshHost^_^
port: ^_^sshPort^_^
timeout: ^_^sshTimeout^_^
username: ^_^sshUsername^_^
password: ^_^sshPassword^_^
privateKey: ^_^sshPrivateKey^_^
privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^
shareConnection: ^_^sshShareConnection^_^
# metrics - cpu
- name: cpu
@@ -1021,6 +1152,16 @@ metrics:
password: ^_^password^_^
# timeout unitms
timeout: ^_^timeout^_^
sshTunnel:
enable: ^_^enableSshTunnel^_^
host: ^_^sshHost^_^
port: ^_^sshPort^_^
timeout: ^_^sshTimeout^_^
username: ^_^sshUsername^_^
password: ^_^sshPassword^_^
privateKey: ^_^sshPrivateKey^_^
privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^
shareConnection: ^_^sshShareConnection^_^
# metrics - errorstats
- name: errorstats
@@ -1057,6 +1198,16 @@ metrics:
password: ^_^password^_^
# timeout unitms
timeout: ^_^timeout^_^
sshTunnel:
enable: ^_^enableSshTunnel^_^
host: ^_^sshHost^_^
port: ^_^sshPort^_^
timeout: ^_^sshTimeout^_^
username: ^_^sshUsername^_^
password: ^_^sshPassword^_^
privateKey: ^_^sshPrivateKey^_^
privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^
shareConnection: ^_^sshShareConnection^_^
# metrics - cluster
- name: cluster
@@ -1086,6 +1237,16 @@ metrics:
password: ^_^password^_^
# timeout unitms
timeout: ^_^timeout^_^
sshTunnel:
enable: ^_^enableSshTunnel^_^
host: ^_^sshHost^_^
port: ^_^sshPort^_^
timeout: ^_^sshTimeout^_^
username: ^_^sshUsername^_^
password: ^_^sshPassword^_^
privateKey: ^_^sshPrivateKey^_^
privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^
shareConnection: ^_^sshShareConnection^_^
# metrics - commandstats
- name: commandstats
@@ -1162,6 +1323,16 @@ metrics:
password: ^_^password^_^
# timeout unitms
timeout: ^_^timeout^_^
sshTunnel:
enable: ^_^enableSshTunnel^_^
host: ^_^sshHost^_^
port: ^_^sshPort^_^
timeout: ^_^sshTimeout^_^
username: ^_^sshUsername^_^
password: ^_^sshPassword^_^
privateKey: ^_^sshPrivateKey^_^
privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^
shareConnection: ^_^sshShareConnection^_^
# metrics - keyspace
- name: keyspace
@@ -1268,3 +1439,13 @@ metrics:
password: ^_^password^_^
# timeout unitms
timeout: ^_^timeout^_^
sshTunnel:
enable: ^_^enableSshTunnel^_^
host: ^_^sshHost^_^
port: ^_^sshPort^_^
timeout: ^_^sshTimeout^_^
username: ^_^sshUsername^_^
password: ^_^sshPassword^_^
privateKey: ^_^sshPrivateKey^_^
privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^
shareConnection: ^_^sshShareConnection^_^
@@ -105,6 +105,78 @@ params:
defaultValue: 3
# hide-is hide this field and put it in advanced layout
hide: true
- field: enableSshTunnel
name:
zh-CN: 是否启用SSH隧道
en-US: Enable SSH Tunnel
type: boolean
required: true
hide: true
- field: sshHost
name:
zh-CN: SSH Host
en-US: SSH Host
type: text
required: false
placeholder: 'When Enable SSH Tunnel'
hide: true
- field: sshPort
name:
zh-CN: SSH端口
en-US: SSH Port
type: number
range: '[0,65535]'
required: false
defaultValue: 22
placeholder: 'When Enable SSH tunnel'
hide: true
- field: sshTimeout
name:
zh-CN: SSH超时时间(ms)
en-US: SSH Timeout(ms)
type: number
required: false
range: '[400,200000]'
defaultValue: 6000
hide: true
- field: sshUsername
name:
zh-CN: SSH用户名
en-US: SSH Username
type: text
required: false
placeholder: 'When Enable SSH tunnel'
hide: true
- field: sshPassword
name:
zh-CN: SSH密码
en-US: SSH Password
type: password
required: false
hide: true
- field: sshShareConnection
name:
zh-CN: 是否共享SSH连接
en-US: Share SSH Connection
type: boolean
required: true
defaultValue: true
hide: true
- field: sshPrivateKey
name:
zh-CN: SSH私钥
en-US: SSH PrivateKey
type: textarea
placeholder: -----BEGIN RSA PRIVATE KEY-----
required: false
hide: true
- field: sshPrivateKeyPassphrase
name:
zh-CN: SSH密钥短语
en-US: SSH PrivateKey PassPhrase
type: password
required: false
hide: true
metrics:
- name: server
# metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel
@@ -247,6 +319,16 @@ metrics:
# timeout unitms
timeout: ^_^timeout^_^
pattern: ^_^pattern^_^
sshTunnel:
enable: ^_^enableSshTunnel^_^
host: ^_^sshHost^_^
port: ^_^sshPort^_^
timeout: ^_^sshTimeout^_^
username: ^_^sshUsername^_^
password: ^_^sshPassword^_^
privateKey: ^_^sshPrivateKey^_^
privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^
shareConnection: ^_^sshShareConnection^_^
- name: clients
@@ -318,6 +400,16 @@ metrics:
# timeout unitms
timeout: ^_^timeout^_^
pattern: ^_^pattern^_^
sshTunnel:
enable: ^_^enableSshTunnel^_^
host: ^_^sshHost^_^
port: ^_^sshPort^_^
timeout: ^_^sshTimeout^_^
username: ^_^sshUsername^_^
password: ^_^sshPassword^_^
privateKey: ^_^sshPrivateKey^_^
privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^
shareConnection: ^_^sshShareConnection^_^
- name: memory
i18n:
@@ -562,6 +654,16 @@ metrics:
# timeout unitms
timeout: ^_^timeout^_^
pattern: ^_^pattern^_^
sshTunnel:
enable: ^_^enableSshTunnel^_^
host: ^_^sshHost^_^
port: ^_^sshPort^_^
timeout: ^_^sshTimeout^_^
username: ^_^sshUsername^_^
password: ^_^sshPassword^_^
privateKey: ^_^sshPrivateKey^_^
privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^
shareConnection: ^_^sshShareConnection^_^
- name: persistence
i18n:
@@ -707,6 +809,16 @@ metrics:
# timeout unitms
timeout: ^_^timeout^_^
pattern: ^_^pattern^_^
sshTunnel:
enable: ^_^enableSshTunnel^_^
host: ^_^sshHost^_^
port: ^_^sshPort^_^
timeout: ^_^sshTimeout^_^
username: ^_^sshUsername^_^
password: ^_^sshPassword^_^
privateKey: ^_^sshPrivateKey^_^
privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^
shareConnection: ^_^sshShareConnection^_^
- name: stats
i18n:
@@ -927,6 +1039,16 @@ metrics:
# timeout unitms
timeout: ^_^timeout^_^
pattern: ^_^pattern^_^
sshTunnel:
enable: ^_^enableSshTunnel^_^
host: ^_^sshHost^_^
port: ^_^sshPort^_^
timeout: ^_^sshTimeout^_^
username: ^_^sshUsername^_^
password: ^_^sshPassword^_^
privateKey: ^_^sshPrivateKey^_^
privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^
shareConnection: ^_^sshShareConnection^_^
- name: replication
i18n:
@@ -1012,6 +1134,16 @@ metrics:
# timeout unitms
timeout: ^_^timeout^_^
pattern: ^_^pattern^_^
sshTunnel:
enable: ^_^enableSshTunnel^_^
host: ^_^sshHost^_^
port: ^_^sshPort^_^
timeout: ^_^sshTimeout^_^
username: ^_^sshUsername^_^
password: ^_^sshPassword^_^
privateKey: ^_^sshPrivateKey^_^
privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^
shareConnection: ^_^sshShareConnection^_^
- name: cpu
i18n:
@@ -1072,6 +1204,16 @@ metrics:
# timeout unitms
timeout: ^_^timeout^_^
pattern: ^_^pattern^_^
sshTunnel:
enable: ^_^enableSshTunnel^_^
host: ^_^sshHost^_^
port: ^_^sshPort^_^
timeout: ^_^sshTimeout^_^
username: ^_^sshUsername^_^
password: ^_^sshPassword^_^
privateKey: ^_^sshPrivateKey^_^
privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^
shareConnection: ^_^sshShareConnection^_^
- name: errorstats
i18n:
@@ -1112,6 +1254,16 @@ metrics:
# timeout unitms
timeout: ^_^timeout^_^
pattern: ^_^pattern^_^
sshTunnel:
enable: ^_^enableSshTunnel^_^
host: ^_^sshHost^_^
port: ^_^sshPort^_^
timeout: ^_^sshTimeout^_^
username: ^_^sshUsername^_^
password: ^_^sshPassword^_^
privateKey: ^_^sshPrivateKey^_^
privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^
shareConnection: ^_^sshShareConnection^_^
- name: cluster
i18n:
@@ -1227,6 +1379,16 @@ metrics:
# timeout unitms
timeout: ^_^timeout^_^
pattern: ^_^pattern^_^
sshTunnel:
enable: ^_^enableSshTunnel^_^
host: ^_^sshHost^_^
port: ^_^sshPort^_^
timeout: ^_^sshTimeout^_^
username: ^_^sshUsername^_^
password: ^_^sshPassword^_^
privateKey: ^_^sshPrivateKey^_^
privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^
shareConnection: ^_^sshShareConnection^_^
- name: commandstats
i18n:
@@ -1307,6 +1469,16 @@ metrics:
# timeout unitms
timeout: ^_^timeout^_^
pattern: ^_^pattern^_^
sshTunnel:
enable: ^_^enableSshTunnel^_^
host: ^_^sshHost^_^
port: ^_^sshPort^_^
timeout: ^_^sshTimeout^_^
username: ^_^sshUsername^_^
password: ^_^sshPassword^_^
privateKey: ^_^sshPrivateKey^_^
privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^
shareConnection: ^_^sshShareConnection^_^
- name: keyspace
@@ -1370,3 +1542,13 @@ metrics:
# timeout unitms
timeout: ^_^timeout^_^
pattern: ^_^pattern^_^
sshTunnel:
enable: ^_^enableSshTunnel^_^
host: ^_^sshHost^_^
port: ^_^sshPort^_^
timeout: ^_^sshTimeout^_^
username: ^_^sshUsername^_^
password: ^_^sshPassword^_^
privateKey: ^_^sshPrivateKey^_^
privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^
shareConnection: ^_^sshShareConnection^_^
@@ -104,6 +104,78 @@ params:
defaultValue: 2
# hide-is hide this field and put it in advanced layout
hide: true
- field: enableSshTunnel
name:
zh-CN: 是否启用SSH隧道
en-US: Enable SSH Tunnel
type: boolean
required: true
hide: true
- field: sshHost
name:
zh-CN: SSH Host
en-US: SSH Host
type: text
required: false
placeholder: 'When Enable SSH Tunnel'
hide: true
- field: sshPort
name:
zh-CN: SSH端口
en-US: SSH Port
type: number
range: '[0,65535]'
required: false
defaultValue: 22
placeholder: 'When Enable SSH tunnel'
hide: true
- field: sshTimeout
name:
zh-CN: SSH超时时间(ms)
en-US: SSH Timeout(ms)
type: number
required: false
range: '[400,200000]'
defaultValue: 6000
hide: true
- field: sshUsername
name:
zh-CN: SSH用户名
en-US: SSH Username
type: text
required: false
placeholder: 'When Enable SSH tunnel'
hide: true
- field: sshPassword
name:
zh-CN: SSH密码
en-US: SSH Password
type: password
required: false
hide: true
- field: sshShareConnection
name:
zh-CN: 是否共享SSH连接
en-US: Share SSH Connection
type: boolean
required: true
defaultValue: true
hide: true
- field: sshPrivateKey
name:
zh-CN: SSH私钥
en-US: SSH PrivateKey
type: textarea
placeholder: -----BEGIN RSA PRIVATE KEY-----
required: false
hide: true
- field: sshPrivateKeyPassphrase
name:
zh-CN: SSH密钥短语
en-US: SSH PrivateKey PassPhrase
type: password
required: false
hide: true
metrics:
- name: server
i18n:
@@ -249,6 +321,16 @@ metrics:
# timeout unitms
timeout: ^_^timeout^_^
pattern: ^_^pattern^_^
sshTunnel:
enable: ^_^enableSshTunnel^_^
host: ^_^sshHost^_^
port: ^_^sshPort^_^
timeout: ^_^sshTimeout^_^
username: ^_^sshUsername^_^
password: ^_^sshPassword^_^
privateKey: ^_^sshPrivateKey^_^
privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^
shareConnection: ^_^sshShareConnection^_^
- name: clients
i18n:
@@ -314,6 +396,16 @@ metrics:
# timeout unitms
timeout: ^_^timeout^_^
pattern: ^_^pattern^_^
sshTunnel:
enable: ^_^enableSshTunnel^_^
host: ^_^sshHost^_^
port: ^_^sshPort^_^
timeout: ^_^sshTimeout^_^
username: ^_^sshUsername^_^
password: ^_^sshPassword^_^
privateKey: ^_^sshPrivateKey^_^
privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^
shareConnection: ^_^sshShareConnection^_^
- name: stats
i18n:
@@ -529,6 +621,16 @@ metrics:
# timeout unitms
timeout: ^_^timeout^_^
pattern: ^_^pattern^_^
sshTunnel:
enable: ^_^enableSshTunnel^_^
host: ^_^sshHost^_^
port: ^_^sshPort^_^
timeout: ^_^sshTimeout^_^
username: ^_^sshUsername^_^
password: ^_^sshPassword^_^
privateKey: ^_^sshPrivateKey^_^
privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^
shareConnection: ^_^sshShareConnection^_^
- name: cpu
i18n:
@@ -584,6 +686,16 @@ metrics:
# timeout unitms
timeout: ^_^timeout^_^
pattern: ^_^pattern^_^
sshTunnel:
enable: ^_^enableSshTunnel^_^
host: ^_^sshHost^_^
port: ^_^sshPort^_^
timeout: ^_^sshTimeout^_^
username: ^_^sshUsername^_^
password: ^_^sshPassword^_^
privateKey: ^_^sshPrivateKey^_^
privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^
shareConnection: ^_^sshShareConnection^_^
- name: sentinel
i18n:
@@ -634,3 +746,13 @@ metrics:
# timeout unitms
timeout: ^_^timeout^_^
pattern: ^_^pattern^_^
sshTunnel:
enable: ^_^enableSshTunnel^_^
host: ^_^sshHost^_^
port: ^_^sshPort^_^
timeout: ^_^sshTimeout^_^
username: ^_^sshUsername^_^
password: ^_^sshPassword^_^
privateKey: ^_^sshPrivateKey^_^
privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^
shareConnection: ^_^sshShareConnection^_^
@@ -80,6 +80,7 @@ excludedResource:
- /api/status/page/public/**===*
# web ui resource
- /===get
- /assets/**===get
- /dashboard/**===get
- /monitors/**===get
- /alert/**===get
@@ -43,7 +43,7 @@ import org.apache.hertzbeat.common.config.CommonConfig;
import org.apache.hertzbeat.common.config.CommonProperties;
import org.apache.hertzbeat.common.queue.impl.InMemoryCommonDataQueue;
import org.apache.hertzbeat.common.support.SpringContextHolder;
import org.apache.hertzbeat.alert.service.TencentSmsClient;
import org.apache.hertzbeat.alert.service.impl.TencentSmsClientImpl;
import org.apache.hertzbeat.warehouse.WarehouseWorkerPool;
import org.apache.hertzbeat.warehouse.controller.MetricsDataController;
import org.apache.hertzbeat.warehouse.store.history.iotdb.IotDbDataStorage;
@@ -93,7 +93,7 @@ class ManagerTest extends AbstractSpringIntegrationTest {
assertNotNull(ctx.getBean(CommonConfig.class));
assertNotNull(ctx.getBean(InMemoryCommonDataQueue.class));
// condition on common.sms.tencent.app-id
assertThrows(NoSuchBeanDefinitionException.class, () -> ctx.getBean(TencentSmsClient.class));
assertThrows(NoSuchBeanDefinitionException.class, () -> ctx.getBean(TencentSmsClientImpl.class));
assertNotNull(ctx.getBean(SpringContextHolder.class));
// test warehouse module
@@ -18,8 +18,11 @@
package org.apache.hertzbeat.warehouse.store;
import java.util.Optional;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.manager.Monitor;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.queue.CommonDataQueue;
import org.apache.hertzbeat.plugin.PostCollectPlugin;
@@ -27,7 +30,6 @@ import org.apache.hertzbeat.plugin.runner.PluginRunner;
import org.apache.hertzbeat.warehouse.WarehouseWorkerPool;
import org.apache.hertzbeat.warehouse.store.history.HistoryDataWriter;
import org.apache.hertzbeat.warehouse.store.realtime.RealTimeDataWriter;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
@@ -44,6 +46,8 @@ public class DataStorageDispatch {
private final RealTimeDataWriter realTimeDataWriter;
private final Optional<HistoryDataWriter> historyDataWriter;
private final PluginRunner pluginRunner;
@PersistenceContext
private EntityManager entityManager;
public DataStorageDispatch(CommonDataQueue commonDataQueue,
WarehouseWorkerPool workerPool,
@@ -87,24 +91,17 @@ public class DataStorageDispatch {
if (metricsData.getPriority() == 0) {
long id = metricsData.getId();
CollectRep.Code code = metricsData.getCode();
// query current status
String queryStatusSql = "SELECT status FROM hzb_monitor WHERE id = ?";
try {
int currentStatus = jdbcTemplate.queryForObject(queryStatusSql, Integer.class, id);
if (code == CollectRep.Code.SUCCESS && currentStatus == CommonConstants.MONITOR_DOWN_CODE) {
// if collect success and current status is DOWN, update to UP
String sql = "UPDATE hzb_monitor SET status = ? WHERE id = ?";
jdbcTemplate.update(sql, CommonConstants.MONITOR_UP_CODE, id);
} else if (code != CollectRep.Code.SUCCESS && currentStatus == CommonConstants.MONITOR_UP_CODE) {
// if collect failed and current status is UP, update to DOWN
String sql = "UPDATE hzb_monitor SET status = ? WHERE id = ?";
jdbcTemplate.update(sql, CommonConstants.MONITOR_DOWN_CODE, id);
String sql = "UPDATE hzb_monitor SET status = ? WHERE id = ? AND status = ?";
int status = code == CollectRep.Code.SUCCESS ? CommonConstants.MONITOR_UP_CODE : CommonConstants.MONITOR_DOWN_CODE;
int preStatus = code == CollectRep.Code.SUCCESS ? CommonConstants.MONITOR_DOWN_CODE : CommonConstants.MONITOR_UP_CODE;
int matchedRows = jdbcTemplate.update(sql, status, id, preStatus);
if (matchedRows > 0) {
entityManager.getEntityManagerFactory().getCache().evict(Monitor.class, id);
}
} catch (EmptyResultDataAccessException ignored) {
// when query currentStatus result is null
} catch (Exception e) {
log.error("Update monitor status failed for monitor id: {}", id, e);
}
}
}
}
+1 -1
View File
@@ -77,7 +77,7 @@ Hi HertzBeat PPMC,
This is a formal vote about inviting ${NEW_COMMITTER_NAME} as our community new committer.
Work list: https://github.com/apache/hertzbeat/commits?author=${NEW_COMMITTER_NAME}
Work list: https://github.com/apache/hertzbeat/commits?author=xxx
Here is the list of ${NEW_COMMITTER_NAME}'s contributions in HertzBeat Community:
+62 -38
View File
@@ -8,52 +8,76 @@ This describes how to configure the SMS server, the number of built-in availabil
**Configuration file `application.yml` of `hertzbeat`**
### Configure the configuration file of HertzBeat
Configuring the HertzBeat configuration file:
Modify the configuration file located at `hertzbeat/config/application.yml`
Note ⚠️The docker container method needs to mount the application.yml file to the local host
The installation package can be decompressed and modified in `hertzbeat/config/application.yml`
- Modify the configuration file located at `hertzbeat/config/application.yml`
- **Docker Deployment:** ⚠️ When using a Docker container, the `application.yml` file must be mounted to the host machine
- **Installation Package Deployment:** Extract the package and modify the configuration file located at `hertzbeat/config/application.yml`
1. Configure the SMS sending server
## 1. Configuring the SMS Sending Service
> Only when your own SMS server is successfully configured, the alarm SMS triggered in the monitoring tool will be sent normally.
Only when you successfully configure your own SMS service will the alert SMS triggered within the monitoring system be sent correctly.
HertzBeat provides two ways to configure the SMS service: modifying the `application.yml` configuration file directly or configuring it through the HertzBeat frontend interface (Settings > Message Server Setting).
Add the following Tencent platform SMS server configuration in `application.yml` (parameters need to be replaced with your SMS server configuration)
> ⚠️ Note: Only one method can be effective at a time. If both methods are configured and enabled, HertzBeat will prioritize the SMS service configured in the frontend interface.
```yaml
common:
sms:
tencent:
secret-id: AKIDbQ4VhdMr89wDedFrIcgU2PaaMvOuBCzY
secret-key: PaXGl0ziY9UcWFjUyiFlCPMr77rLkJYlyA
app-id: 1435441637
sign-name: XX Technology
template-id: 1343434
```
### 1.1 Tencent Cloud SMS Configuration
2. Configure alarm custom parameters
Add the following Tencent Cloud SMS server configuration to `application.yml` (replace parameters with your own SMS server configuration):
```yaml
alerter:
# Custom console address
console-url: https://console.tancloud.io
```
```yaml
alerter:
sms:
enable: true # Whether to enable
type: tencent # SMS provider type, supports "tencent"
tencent: # Tencent Cloud SMS configuration
secret-id: AKIDbQ4VhdMr89wDedFrIcgU2PaaMvOuBCzY
secret-key: PaXGl0ziY9UcWFjUyiFlCPMr77rLkJYlyA
app-id: 1435441637
sign-name: HertzBeat
template-id: 1343434
```
3. Use external redis instead of memory to store real-time metric data
1. Create a signature (sign-name) in Tencent Cloud SMS
![image](https://github.com/apache/hertzbeat/assets/40455946/3a4c287d-b23d-4398-8562-4894296af485)
> By default, the real-time data of our metrics is stored in memory, which can be configured as follows to use redis instead of memory storage.
2. Create a message template (template-id) in Tencent Cloud SMS
Note ⚠️ `memory.enabled: false, redis.enabled: true`
```text
Monitor: {1}, Alert Level: {2}. Content: {3}
```
```yaml
warehouse:
store:
memory:
enabled: false
init-size: 1024
redis:
enabled: true
host: 127.0.0.1
port: 6379
password: 123456
```
![image](https://github.com/apache/hertzbeat/assets/40455946/face71a6-46d5-452c-bed3-59d2a975afeb)
3. Create an application (app-id) in Tencent Cloud SMS
![image](https://github.com/apache/hertzbeat/assets/40455946/2732d710-37fa-4455-af64-48bba273c2f8)
4. Obtain Tencent Cloud Access Management credentials (secret-id, secret-key)
![image](https://github.com/apache/hertzbeat/assets/40455946/36f056f0-94e7-43db-8f07-82893c98024e)
## 2. Configuring Custom Alert Parameters
```yaml
alerter:
# Custom console URL
console-url: https://console.tancloud.io
```
## 3. Using an External Redis Instead of In-Memory Storage for Real-Time Metric Data
> By default, real-time metric data is stored in memory. You can configure Redis as a replacement using the settings below.
⚠️ Note: Set `memory.enabled: false, redis.enabled: true`
```yaml
warehouse:
store:
memory:
enabled: false
init-size: 1024
redis:
enabled: true
host: 127.0.0.1
port: 6379
password: 123456
```
@@ -77,7 +77,7 @@ Hi HertzBeat PPMC,
This is a formal vote about inviting ${NEW_COMMITTER_NAME} as our community new committer.
Work list: https://github.com/apache/hertzbeat/commits?author=${NEW_COMMITTER_NAME}
Work list: https://github.com/apache/hertzbeat/commits?author=xxx
Here is the list of ${NEW_COMMITTER_NAME}'s contributions in HertzBeat Community:
@@ -4,73 +4,79 @@ title: 常见参数配置
sidebar_label: 常见参数配置
---
这里描述了如配置短信服务,内置可用性告警触发次数等。
这里描述了如配置短信服务,内置可用性告警触发次数等。
**`hertzbeat`的配置文件`application.yml`**
### 配置HertzBeat的配置文件
配置HertzBeat的配置文件
修改位于 `hertzbeat/config/application.yml` 的配置文件
注意⚠️docker容器方式需要将application.yml文件挂载到主机本地
安装包方式解压修改位于 `hertzbeat/config/application.yml` 即可
- 修改位于 `hertzbeat/config/application.yml` 的配置文件
- **Docker部署:** ⚠️docker容器方式需要将 `application.yml` 文件挂载到主机本地
- **安装包方式** 解压修改位于 `hertzbeat/config/application.yml` 的配置文件即可
1. 配置短信发送服务
## 1. 配置短信发送服务
> 只有成功配置了您自己的短信服务,监控系统内触发的告警短信才会正常发送。
只有成功配置了您自己的短信服务,监控系统内触发的告警短信才会正常发送。
hertzbeat有两种方式配置短信服务,一种是直接修改`application.yml`配置文件,另一种是通过hertzbeat前端界面(系统设置 > 消息服务配置)配置。
> 注意⚠️:两种方式配置的短信服务只能选择一种生效,当两种方式都配置并且开启时,hertzbeat将会优先使用前端界面配置的短信服务。
`application.yml`新增如下腾讯平台短信服务器配置(参数需替换为您的短信服务器配置)
### 1.1 腾讯云短信配置
```yaml
common:
sms:
tencent:
secret-id: AKIDbQ4VhdMr89wDedFrIcgU2PaaMvOuBCzY
secret-key: PaXGl0ziY9UcWFjUyiFlCPMr77rLkJYlyA
app-id: 1435441637
sign-name: 赫兹跳动
template-id: 1343434
```
`application.yml`新增如下腾讯平台短信服务器配置(参数需替换为您的短信服务器配置)
1.1 腾讯云短信创建签名(sign-name)
![image](https://github.com/apache/hertzbeat/assets/40455946/3a4c287d-b23d-4398-8562-4894296af485)
```yaml
alerter:
sms:
enable: true # 是否启用
type: tencent # 短信服务商类型,支持tencent、
tencent: # 腾讯云短信配置
secret-id: AKIDbQ4VhdMr89wDedFrIcgU2PaaMvOuBCzY
secret-key: PaXGl0ziY9UcWFjUyiFlCPMr77rLkJYlyA
app-id: 1435441637
sign-name: 赫兹跳动
template-id: 1343434
```
1.2 腾讯云短信创建正文模板(template-id
1. 腾讯云短信创建签名(sign-name
![image](https://github.com/apache/hertzbeat/assets/40455946/3a4c287d-b23d-4398-8562-4894296af485)
```text
监控:{1},告警级别:{2}。内容:{3}
```
2. 腾讯云短信创建正文模板(template-id
![image](https://github.com/apache/hertzbeat/assets/40455946/face71a6-46d5-452c-bed3-59d2a975afeb)
```text
监控:{1},告警级别:{2}。内容:{3}
```
1.3 腾讯云短信创建应用(app-id)
![image](https://github.com/apache/hertzbeat/assets/40455946/2732d710-37fa-4455-af64-48bba273c2f8)
![image](https://github.com/apache/hertzbeat/assets/40455946/face71a6-46d5-452c-bed3-59d2a975afeb)
1.4 腾讯云访问管理(secret-id、secret-key
![image](https://github.com/apache/hertzbeat/assets/40455946/36f056f0-94e7-43db-8f07-82893c98024e)
3. 腾讯云短信创建应用(app-id
![image](https://github.com/apache/hertzbeat/assets/40455946/2732d710-37fa-4455-af64-48bba273c2f8)
2. 配置告警自定义参数
4. 腾讯云访问管理(secret-id、secret-key
![image](https://github.com/apache/hertzbeat/assets/40455946/36f056f0-94e7-43db-8f07-82893c98024e)
```yaml
alerter:
# 自定义控制台地址
console-url: https://console.tancloud.io
```
## 2. 配置告警自定义参数
3. 使用外置redis代替内存存储实时指标数据
```yaml
alerter:
# 自定义控制台地址
console-url: https://console.tancloud.io
```
> 默认我们的指标实时数据存储在内存中,可以配置如下来使用redis代替内存存储
## 3. 使用外置redis代替内存存储实时指标数据
注意⚠️ `memory.enabled: false, redis.enabled: true`
> 默认我们的指标实时数据存储在内存中,可以配置如下来使用redis代替内存存储。
```yaml
warehouse:
store:
memory:
enabled: false
init-size: 1024
redis:
enabled: true
host: 127.0.0.1
port: 6379
password: 123456
```
注意⚠️ `memory.enabled: false, redis.enabled: true`
```yaml
warehouse:
store:
memory:
enabled: false
init-size: 1024
redis:
enabled: true
host: 127.0.0.1
port: 6379
password: 123456
```
+3 -4
View File
@@ -235,8 +235,6 @@ The text of each license is the standard Apache 2.0 license.
https://mvnrepository.com/artifact/com.squareup.okio/okio-jvm/3.6.0 Apache-2.0
https://mvnrepository.com/artifact/com.squareup.retrofit2/converter-moshi/2.9.0 Apache-2.0
https://mvnrepository.com/artifact/com.squareup.retrofit2/retrofit/2.9.0 Apache-2.0
https://mvnrepository.com/artifact/com.tencentcloudapi/tencentcloud-sdk-java-common/3.1.648 Apache-2.0
https://mvnrepository.com/artifact/com.tencentcloudapi/tencentcloud-sdk-java-sms/3.1.648 Apache-2.0
https://mvnrepository.com/artifact/com.usthe.sureness/spring-boot3-starter-sureness/1.1.0 Apache-2.0
https://mvnrepository.com/artifact/com.usthe.sureness/sureness-core/1.1.0 Apache-2.0
https://mvnrepository.com/artifact/com.zaxxer/HikariCP/5.0.1 Apache-2.0
@@ -352,8 +350,9 @@ The text of each license is the standard Apache 2.0 license.
https://mvnrepository.com/artifact/org.apache.rocketmq/rocketmq-remoting/4.9.4 Apache-2.0
https://mvnrepository.com/artifact/org.apache.rocketmq/rocketmq-srvutil/4.9.4 Apache-2.0
https://mvnrepository.com/artifact/org.apache.rocketmq/rocketmq-tools/4.9.4 Apache-2.0
https://mvnrepository.com/artifact/org.apache.sshd/sshd-common/2.8.0 Apache-2.0
https://mvnrepository.com/artifact/org.apache.sshd/sshd-core/2.8.0 Apache-2.0
https://mvnrepository.com/artifact/org.apache.sshd/sshd-common/2.13.1 Apache-2.0
https://mvnrepository.com/artifact/org.apache.sshd/sshd-core/2.13.1 Apache-2.0
https://mvnrepository.com/artifact/org.apache.sshd/sshd-sftp/2.13.1 Apache-2.0
https://mvnrepository.com/artifact/org.apache.tomcat.embed/tomcat-embed-el/10.1.19 Apache-2.0
https://mvnrepository.com/artifact/org.apache.tomcat.embed/tomcat-embed-websocket/10.1.19 Apache-2.0
https://mvnrepository.com/artifact/org.apache.xmlbeans/xmlbeans/3.1.0 Apache-2.0
+3 -4
View File
@@ -235,8 +235,6 @@ The text of each license is the standard Apache 2.0 license.
https://mvnrepository.com/artifact/com.squareup.okio/okio-jvm/3.6.0 Apache-2.0
https://mvnrepository.com/artifact/com.squareup.retrofit2/converter-moshi/2.9.0 Apache-2.0
https://mvnrepository.com/artifact/com.squareup.retrofit2/retrofit/2.9.0 Apache-2.0
https://mvnrepository.com/artifact/com.tencentcloudapi/tencentcloud-sdk-java-common/3.1.648 Apache-2.0
https://mvnrepository.com/artifact/com.tencentcloudapi/tencentcloud-sdk-java-sms/3.1.648 Apache-2.0
https://mvnrepository.com/artifact/com.usthe.sureness/spring-boot3-starter-sureness/1.1.0 Apache-2.0
https://mvnrepository.com/artifact/com.usthe.sureness/sureness-core/1.1.0 Apache-2.0
https://mvnrepository.com/artifact/com.zaxxer/HikariCP/5.0.1 Apache-2.0
@@ -352,8 +350,9 @@ The text of each license is the standard Apache 2.0 license.
https://mvnrepository.com/artifact/org.apache.rocketmq/rocketmq-remoting/4.9.4 Apache-2.0
https://mvnrepository.com/artifact/org.apache.rocketmq/rocketmq-srvutil/4.9.4 Apache-2.0
https://mvnrepository.com/artifact/org.apache.rocketmq/rocketmq-tools/4.9.4 Apache-2.0
https://mvnrepository.com/artifact/org.apache.sshd/sshd-common/2.8.0 Apache-2.0
https://mvnrepository.com/artifact/org.apache.sshd/sshd-core/2.8.0 Apache-2.0
https://mvnrepository.com/artifact/org.apache.sshd/sshd-common/2.13.1 Apache-2.0
https://mvnrepository.com/artifact/org.apache.sshd/sshd-core/2.13.1 Apache-2.0
https://mvnrepository.com/artifact/org.apache.sshd/sshd-sftp/2.13.1 Apache-2.0
https://mvnrepository.com/artifact/org.apache.tomcat.embed/tomcat-embed-el/10.1.19 Apache-2.0
https://mvnrepository.com/artifact/org.apache.tomcat.embed/tomcat-embed-websocket/10.1.19 Apache-2.0
https://mvnrepository.com/artifact/org.apache.xmlbeans/xmlbeans/3.1.0 Apache-2.0
+3 -2
View File
@@ -284,8 +284,9 @@ The text of each license is the standard Apache 2.0 license.
https://mvnrepository.com/artifact/org.apache.rocketmq/rocketmq-remoting/4.9.4 Apache-2.0
https://mvnrepository.com/artifact/org.apache.rocketmq/rocketmq-srvutil/4.9.4 Apache-2.0
https://mvnrepository.com/artifact/org.apache.rocketmq/rocketmq-tools/4.9.4 Apache-2.0
https://mvnrepository.com/artifact/org.apache.sshd/sshd-common/2.8.0 Apache-2.0
https://mvnrepository.com/artifact/org.apache.sshd/sshd-core/2.8.0 Apache-2.0
https://mvnrepository.com/artifact/org.apache.sshd/sshd-common/2.13.1 Apache-2.0
https://mvnrepository.com/artifact/org.apache.sshd/sshd-core/2.13.1 Apache-2.0
https://mvnrepository.com/artifact/org.apache.sshd/sshd-sftp/2.13.1 Apache-2.0
https://mvnrepository.com/artifact/org.apache.tomcat.embed/tomcat-embed-el/10.1.19 Apache-2.0
https://mvnrepository.com/artifact/org.apache.tomcat.embed/tomcat-embed-websocket/10.1.19 Apache-2.0
https://mvnrepository.com/artifact/org.apache.plc4x/plc4j-api/0.12.0 Apache-2.0
+9 -16
View File
@@ -23,7 +23,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.3</version>
<version>3.4.2</version>
</parent>
<groupId>org.apache.hertzbeat</groupId>
@@ -112,14 +112,12 @@
<springdoc.version>2.3.0</springdoc.version>
<spring-boot-starter-sureness.version>1.1.0</spring-boot-starter-sureness.version>
<javaparser.version>3.26.1</javaparser.version>
<nekohtml.version>1.9.22</nekohtml.version>
<json-path.version>2.9.0</json-path.version>
<gson.version>2.10.1</gson.version>
<guava.version>32.1.2-jre</guava.version>
<protobuf.version>3.25.5</protobuf.version>
<tencentcloud-sdk-java-sms.version>3.1.648</tencentcloud-sdk-java-sms.version>
<aliYun-sdk-java-sms.version>2.0.24</aliYun-sdk-java-sms.version>
<caffeine.version>2.9.3</caffeine.version>
<httpclient.version>4.5.14</httpclient.version>
@@ -170,10 +168,11 @@
<influxdb.version>2.23</influxdb.version>
<spring-cloud-starter-openfeign.version>3.0.5</spring-cloud-starter-openfeign.version>
<taos-jdbcdriver.version>3.0.0</taos-jdbcdriver.version>
<greptimedb.version>0.9.1</greptimedb.version>
<greptimedb.version>0.11.0</greptimedb.version>
<mysql-jdbcdriver.version>8.0.33</mysql-jdbcdriver.version>
<arrow.version>18.1.0</arrow.version>
<snappy-java.version>1.1.10.7</snappy-java.version>
<sshd-sftp.version>2.13.1</sshd-sftp.version>
</properties>
<dependencyManagement>
@@ -421,17 +420,6 @@
</exclusion>
</exclusions>
</dependency>
<!-- sms -->
<dependency>
<groupId>com.tencentcloudapi</groupId>
<artifactId>tencentcloud-sdk-java-sms</artifactId>
<version>${tencentcloud-sdk-java-sms.version}</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>dysmsapi20170525</artifactId>
<version>${aliYun-sdk-java-sms.version}</version>
</dependency>
<!-- okhttp -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
@@ -474,6 +462,11 @@
<artifactId>arrow-memory-netty</artifactId>
<version>${arrow.version}</version>
</dependency>
<dependency>
<groupId>org.apache.sshd</groupId>
<artifactId>sshd-sftp</artifactId>
<version>${sshd-sftp.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
+22
View File
@@ -202,6 +202,28 @@ alerter:
# alert inhibit ttl unit ms, default 14400000(4 hours)
inhibit:
ttl: 14400000
sms:
enable: false
type: tencent
tencent:
secret-id:
secret-key:
app-id:
sign-name:
template-id:
alibaba:
access-key-id:
access-key-secret:
sign-name:
template-code:
unisms:
# auth-mode: simple or hmac
auth-mode: simple
access-key-id: YOUR_ACCESS_KEY_ID
# hmac mode need to fill in access-key-secret
access-key-secret: YOUR_ACCESS_KEY_SECRET
signature: YOUR_SMS_SIGNATURE
template-id: YOUR_TEMPLATE_ID
scheduler:
server:
-5
View File
@@ -51,11 +51,6 @@
<property name="message"
value="Consider using special escape sequence instead of octal value or Unicode escaped value."/>
</module>
<module name="AvoidEscapedUnicodeCharacters">
<property name="allowEscapesForControlCharacters" value="true"/>
<property name="allowByTailComment" value="true"/>
<property name="allowNonPrintableEscapes" value="true"/>
</module>
<module name="OneTopLevelClass"/>
<module name="NoLineWrap">
<property name="tokens" value="PACKAGE_DEF, IMPORT, STATIC_IMPORT"/>
@@ -169,6 +169,28 @@ alerter:
# alert inhibit ttl unit ms, default 14400000(4 hours)
inhibit:
ttl: 14400000
sms:
enable: false
type: tencent
tencent:
secret-id:
secret-key:
app-id:
sign-name:
template-id:
alibaba:
access-key-id:
access-key-secret:
sign-name:
template-code:
unisms:
# auth-mode: simple or hmac
auth-mode: simple
access-key-id: YOUR_ACCESS_KEY_ID
# hmac mode need to fill in access-key-secret
access-key-secret: YOUR_ACCESS_KEY_SECRET
signature: YOUR_SMS_SIGNATURE
template-id: YOUR_TEMPLATE_ID
scheduler:
server:
@@ -72,6 +72,7 @@ resourceRole:
# eg: /api/v1/source3===get means /api/v1/source3===get can be access by anyone, no need auth.
excludedResource:
- /api/alerts/report/**===*
- /api/alert/sse/**===*
- /api/account/auth/**===*
- /api/i18n/**===get
- /api/apps/hierarchy===get
@@ -79,6 +80,7 @@ excludedResource:
- /api/status/page/public/**===*
# web ui resource
- /===get
- /assets/**===get
- /dashboard/**===get
- /monitors/**===get
- /alert/**===get
@@ -165,6 +165,28 @@ alerter:
# alert inhibit ttl unit ms, default 14400000(4 hours)
inhibit:
ttl: 14400000
sms:
enable: false
type: tencent
tencent:
secret-id:
secret-key:
app-id:
sign-name:
template-id:
alibaba:
access-key-id:
access-key-secret:
sign-name:
template-code:
unisms:
# auth-mode: simple or hmac
auth-mode: simple
access-key-id: YOUR_ACCESS_KEY_ID
# hmac mode need to fill in access-key-secret
access-key-secret: YOUR_ACCESS_KEY_SECRET
signature: YOUR_SMS_SIGNATURE
template-id: YOUR_TEMPLATE_ID
scheduler:
server:
@@ -72,6 +72,7 @@ resourceRole:
# eg: /api/v1/source3===get means /api/v1/source3===get can be access by anyone, no need auth.
excludedResource:
- /api/alerts/report/**===*
- /api/alert/sse/**===*
- /api/account/auth/**===*
- /api/i18n/**===get
- /api/apps/hierarchy===get
@@ -79,6 +80,7 @@ excludedResource:
- /api/status/page/public/**===*
# web ui resource
- /===get
- /assets/**===get
- /dashboard/**===get
- /monitors/**===get
- /alert/**===get
@@ -163,6 +163,28 @@ alerter:
# alert inhibit ttl unit ms, default 14400000(4 hours)
inhibit:
ttl: 14400000
sms:
enable: false
type: tencent
tencent:
secret-id:
secret-key:
app-id:
sign-name:
template-id:
alibaba:
access-key-id:
access-key-secret:
sign-name:
template-code:
unisms:
# auth-mode: simple or hmac
auth-mode: simple
access-key-id: YOUR_ACCESS_KEY_ID
# hmac mode need to fill in access-key-secret
access-key-secret: YOUR_ACCESS_KEY_SECRET
signature: YOUR_SMS_SIGNATURE
template-id: YOUR_TEMPLATE_ID
scheduler:
server:
@@ -72,6 +72,7 @@ resourceRole:
# eg: /api/v1/source3===get means /api/v1/source3===get can be access by anyone, no need auth.
excludedResource:
- /api/alerts/report/**===*
- /api/alert/sse/**===*
- /api/account/auth/**===*
- /api/i18n/**===get
- /api/apps/hierarchy===get
@@ -79,6 +80,7 @@ excludedResource:
- /api/status/page/public/**===*
# web ui resource
- /===get
- /assets/**===get
- /dashboard/**===get
- /monitors/**===get
- /alert/**===get
@@ -162,6 +162,28 @@ alerter:
# alert inhibit ttl unit ms, default 14400000(4 hours)
inhibit:
ttl: 14400000
sms:
enable: false
type: tencent
tencent:
secret-id:
secret-key:
app-id:
sign-name:
template-id:
alibaba:
access-key-id:
access-key-secret:
sign-name:
template-code:
unisms:
# auth-mode: simple or hmac
auth-mode: simple
access-key-id: YOUR_ACCESS_KEY_ID
# hmac mode need to fill in access-key-secret
access-key-secret: YOUR_ACCESS_KEY_SECRET
signature: YOUR_SMS_SIGNATURE
template-id: YOUR_TEMPLATE_ID
scheduler:
server:
@@ -72,6 +72,7 @@ resourceRole:
# eg: /api/v1/source3===get means /api/v1/source3===get can be access by anyone, no need auth.
excludedResource:
- /api/alerts/report/**===*
- /api/alert/sse/**===*
- /api/account/auth/**===*
- /api/i18n/**===get
- /api/apps/hierarchy===get
@@ -79,6 +80,7 @@ excludedResource:
- /api/status/page/public/**===*
# web ui resource
- /===get
- /assets/**===get
- /dashboard/**===get
- /monitors/**===get
- /alert/**===get
+2
View File
@@ -72,6 +72,7 @@ resourceRole:
# eg: /api/v1/source3===get means /api/v1/source3===get can be access by anyone, no need auth.
excludedResource:
- /api/alerts/report/**===*
- /api/alert/sse/**===*
- /api/account/auth/**===*
- /api/i18n/**===get
- /api/apps/hierarchy===get
@@ -79,6 +80,7 @@ excludedResource:
- /api/status/page/public/**===*
# web ui resource
- /===get
- /assets/**===get
- /dashboard/**===get
- /monitors/**===get
- /alert/**===get
-6
View File
@@ -19,7 +19,6 @@ export class AppComponent implements OnInit {
private router: Router,
private titleSrv: TitleService,
private modalSrv: NzModalService,
private themeService: ThemeService,
@Inject(ALAIN_I18N_TOKEN) private i18nSvc: I18NService
) {
renderer.setAttribute(el.nativeElement, 'ng-alain-version', VERSION_ALAIN.full);
@@ -49,10 +48,5 @@ export class AppComponent implements OnInit {
this.modalSrv.closeAll();
}
});
// set theme
const storedTheme = localStorage.getItem('theme');
if (storedTheme) {
this.themeService.changeTheme(storedTheme);
}
}
}
+13 -3
View File
@@ -2,6 +2,7 @@ import { Platform } from '@angular/cdk/platform';
import { registerLocaleData } from '@angular/common';
import { HttpHeaders } from '@angular/common/http';
import ngEn from '@angular/common/locales/en';
import ngJa from '@angular/common/locales/ja';
import ngZh from '@angular/common/locales/zh';
import ngZhTw from '@angular/common/locales/zh-Hant';
import { Injectable } from '@angular/core';
@@ -12,12 +13,13 @@ import {
en_US as delonEnUS,
SettingsService,
zh_CN as delonZhCn,
zh_TW as delonZhTw
zh_TW as delonZhTw,
ja_JP as delonJaJP
} from '@delon/theme';
import { AlainConfigService } from '@delon/util/config';
import { enUS as dfEn, zhCN as dfZhCn, zhTW as dfZhTw } from 'date-fns/locale';
import { enUS as dfEn, zhCN as dfZhCn, zhTW as dfZhTw, ja as dfJa } from 'date-fns/locale';
import { NzSafeAny } from 'ng-zorro-antd/core/types';
import { en_US as zorroEnUS, NzI18nService, zh_CN as zorroZhCN, zh_TW as zorroZhTW } from 'ng-zorro-antd/i18n';
import { en_US as zorroEnUS, NzI18nService, zh_CN as zorroZhCN, zh_TW as zorroZhTW, ja_JP as zorroJaJP } from 'ng-zorro-antd/i18n';
import { Observable, zip } from 'rxjs';
import { map } from 'rxjs/operators';
@@ -57,6 +59,14 @@ const LANGS: { [key: string]: LangConfigData } = {
date: dfZhTw,
delon: delonZhTw,
abbr: '🇭🇰'
},
'ja-JP': {
text: '日本語',
ng: ngJa,
zorro: zorroJaJP,
date: dfJa,
delon: delonJaJP,
abbr: '🇯🇵'
}
};
@@ -12,6 +12,7 @@ import { catchError, map } from 'rxjs/operators';
import { ICONS } from '../../../style-icons';
import { ICONS_AUTO } from '../../../style-icons-auto';
import { MemoryStorageService } from '../../service/memory-storage.service';
import { ThemeService } from '../../service/theme.service';
import { I18NService } from '../i18n/i18n.service';
@Injectable({
@@ -28,7 +29,8 @@ export class StartupService {
@Inject(DA_SERVICE_TOKEN) private tokenService: ITokenService,
private httpClient: HttpClient,
private router: Router,
private storageService: MemoryStorageService
private storageService: MemoryStorageService,
private themeService: ThemeService
) {
iconSrv.addIcon(...ICONS_AUTO, ...ICONS);
}
@@ -86,6 +88,7 @@ export class StartupService {
this.storageService.putData('hierarchy', menuData.data);
this.menuService.resume();
this.titleService.suffix = appData.app.name;
this.themeService.changeTheme(null);
})
);
}
@@ -41,9 +41,6 @@ import { CONSTANTS } from '../../shared/constants';
<div nz-menu-item>
<header-fullscreen></header-fullscreen>
</div>
<div nz-menu-item>
<header-clear-storage></header-clear-storage>
</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>
@@ -1,35 +0,0 @@
import { ChangeDetectionStrategy, Component, HostListener, Inject } from '@angular/core';
import { I18NService } from '@core';
import { ALAIN_I18N_TOKEN } from '@delon/theme';
import { NzMessageService } from 'ng-zorro-antd/message';
import { NzModalService } from 'ng-zorro-antd/modal';
@Component({
selector: 'header-clear-storage',
template: `
<i nz-icon class="mr-sm" nzType="tool"></i>
{{ 'menu.clear.local.storage' | i18n }}
`,
host: {
'[class.d-block]': 'true'
},
changeDetection: ChangeDetectionStrategy.OnPush
})
export class HeaderClearStorageComponent {
constructor(
private modalSrv: NzModalService,
private messageSrv: NzMessageService,
@Inject(ALAIN_I18N_TOKEN) private i18nSvc: I18NService
) {}
@HostListener('click')
_click(): void {
this.modalSrv.confirm({
nzTitle: this.i18nSvc.fanyi('common.confirm.clear-cache'),
nzOnOk: () => {
localStorage.clear();
this.messageSrv.success(this.i18nSvc.fanyi('common.notify.clear-success'));
}
});
}
}
@@ -129,12 +129,7 @@ export class HeaderUserComponent {
}
logout(): void {
let tmp = this.localStorageSvc.getData(this.notShowAgainKey);
if (tmp === null) {
tmp = 'false';
}
this.localStorageSvc.clear();
this.localStorageSvc.putData(this.notShowAgainKey, tmp);
this.localStorageSvc.clearAuthorization();
this.router.navigateByUrl('/passport/login');
}
-2
View File
@@ -19,7 +19,6 @@ import { NzInputModule } from 'ng-zorro-antd/input';
import { NzSpinModule } from 'ng-zorro-antd/spin';
import { LayoutBasicComponent } from './basic/basic.component';
import { HeaderClearStorageComponent } from './basic/widgets/clear-storage.component';
import { HeaderFullScreenComponent } from './basic/widgets/fullscreen.component';
import { HeaderI18nComponent } from './basic/widgets/i18n.component';
import { HeaderSearchComponent } from './basic/widgets/search.component';
@@ -33,7 +32,6 @@ const HEADER_COMPONENTS = [
HeaderSearchComponent,
HeaderFullScreenComponent,
HeaderI18nComponent,
HeaderClearStorageComponent,
HeaderUserComponent,
HeaderNotifyComponent
];
+3 -4
View File
@@ -18,9 +18,8 @@
*/
export class AlibabaSmsConfig {
secretId!: string;
secretKey!: string;
accessKeyId!: string;
accessKeySecret!: string;
signName!: string;
appId!: string;
templateId!: string;
templateCode!: string;
}
+7 -4
View File
@@ -19,13 +19,16 @@
import { AlibabaSmsConfig } from './AlibabaSmsConfig';
import { TencentSmsConfig } from './TencentSmsConfig';
import { UniSmsConfig } from './UniSmsConfig';
import { SmsType } from './enums/sms-type.enum';
export class SmsNoticeSender {
id!: number;
type!: string;
tencent!: TencentSmsConfig;
alibaba!: AlibabaSmsConfig;
enable!: boolean;
type: SmsType = SmsType.TENCENT;
tencent: TencentSmsConfig = new TencentSmsConfig();
alibaba: AlibabaSmsConfig = new AlibabaSmsConfig();
unisms: UniSmsConfig = new UniSmsConfig();
enable: boolean = false;
creator!: string;
modifier!: string;
gmtCreate!: number;
+26
View File
@@ -0,0 +1,26 @@
/*
* 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.
*/
export class UniSmsConfig {
accessKeyId!: string;
accessKeySecret!: string;
signature!: string;
authMode!: string;
templateId!: string;
}
@@ -0,0 +1,29 @@
/*
* 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.
*/
export enum SmsType {
TENCENT = 'tencent',
ALIBABA = 'alibaba',
UNISMS = 'unisms'
}
export enum UniSmsAuthMode {
HMAC = 'hmac',
SIMPLE = 'simple'
}
@@ -1,3 +1,5 @@
@import "~src/styles/theme";
:host ::ng-deep app-toolbar {
.center-content {
display: flex;
@@ -36,22 +38,6 @@
border-radius: 6px;
padding: 4px 11px 4px 40px;
}
.ant-input-prefix {
color: rgba(0, 0, 0, 0.45);
font-size: 16px;
margin-right: 8px;
margin-left: 12px;
}
.ant-input-affix-wrapper {
border-radius: 6px;
&:hover, &:focus {
border-color: #40a9ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
}
}
}
@@ -67,17 +53,40 @@
flex-direction: column;
gap: 12px;
padding: 16px;
transform-style: preserve-3d;
perspective: 1200px;
}
.alert-card {
background: #fff;
position: relative;
background: @common-background-color;
border-radius: 4px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
border-left: 4px solid #ff4d4f;
transition: all 0.3s;
z-index: 1;
&.expanded {
z-index: 2;
}
&.status-firing {
border-left: 4px solid #ff4d4f;
opacity: 0.9;
}
&.status-resolved {
border-left: 4px solid #52c41a;
opacity: 0.7;
}
&.status-pending {
border-left: 4px solid #faad14;
opacity: 0.8;
}
&:hover {
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.15);
z-index: 3;
}
::ng-deep .ant-card-body {
@@ -89,7 +98,7 @@
justify-content: space-between;
align-items: flex-start;
padding-bottom: 12px;
border-bottom: 1px solid #f0f0f0;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
.alert-info {
flex: 1;
@@ -109,7 +118,7 @@
display: flex;
gap: 16px;
margin-top: 8px;
color: #8c8c8c;
color: rgba(0, 0, 0, 0.5);
font-size: 12px;
i {
@@ -142,16 +151,22 @@
.alert-details {
margin-top: 12px;
position: relative;
z-index: 4;
::ng-deep {
.ant-collapse {
background: transparent;
border: none;
position: relative;
z-index: 5;
.ant-collapse-item {
border-radius: 2px;
border: 1px solid #f0f0f0;
border: 1px solid rgba(0, 0, 0, 0.1);
margin-bottom: 8px;
position: relative;
z-index: 6;
&:last-child {
margin-bottom: 0;
@@ -161,9 +176,11 @@
padding: 8px 12px;
align-items: center;
cursor: pointer;
position: relative;
z-index: 7;
&:hover {
background-color: #fafafa;
background-color: rgba(0, 0, 0, 0.05);
}
.ant-collapse-header-text {
@@ -172,19 +189,20 @@
.ant-collapse-extra {
margin: 0;
color: #8c8c8c;
color: rgba(0, 0, 0, 0.5);
font-size: 12px;
}
.alert-content {
font-size: 13px;
color: #333;
line-height: 1.5;
}
}
.ant-collapse-content {
border-top: 1px solid #f0f0f0;
border-top: 1px solid rgba(0, 0, 0, 0.1);
position: relative;
z-index: 6;
.ant-collapse-content-box {
padding: 12px;
@@ -268,36 +286,147 @@
text-align: right;
}
.alert-card {
&.status-firing {
border-left-color: #ff4d4f;
@keyframes slideInFromRight {
0% {
transform: translate3d(120%, 0, 0) scale(0.95) rotate(3deg);
opacity: 0;
filter: blur(2px);
box-shadow: 0 24px 48px -12px rgba(0, 0, 0, 0.18);
}
&.status-resolved {
border-left-color: #52c41a;
50% {
opacity: 1;
filter: blur(0);
transform: translate3d(-5%, 0, 0) scale(1) rotate(-1deg);
}
&.status-pending {
border-left-color: #faad14;
75% {
transform: translate3d(2%, 0, 0) scale(1) rotate(0.5deg);
}
100% {
transform: translate3d(0, 0, 0) scale(1) rotate(0deg);
box-shadow: 0 8px 16px -4px rgba(0, 0, 0, 0.1);
}
}
@keyframes slideInFromRight {
.alert-card {
transition:
transform 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94),
opacity 0.5s ease-out,
box-shadow 0.3s ease;
will-change: transform, opacity;
&:hover {
transform: translateY(-2px) scale(1.005);
box-shadow: 0 12px 24px -8px rgba(0, 0, 0, 0.15);
}
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(
90deg,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.3) 50%,
rgba(255, 255, 255, 0) 100%
);
opacity: 0;
animation: slideGlow 1s cubic-bezier(0.4, 0, 0.2, 1);
pointer-events: none;
}
&.new-alert {
animation:
slideInFromRight 0.8s cubic-bezier(0.34, 1.56, 0.64, 1) forwards,
cardLanding 0.6s 0.3s ease-out forwards;
}
}
@keyframes slideGlow {
0% {
transform: translateX(-100%);
opacity: 0;
}
50% {
opacity: 0.6;
}
100% {
transform: translateX(100%);
opacity: 0;
}
100% {
transform: translateX(0);
opacity: 1;
}
}
.alert-card {
position: relative;
transition: transform 0.5s ease-out, opacity 0.5s ease-out;
@keyframes cardLanding {
0% { transform: translateY(0); }
50% { transform: translateY(-8px); }
80% { transform: translateY(2px); }
100% { transform: translateY(0); }
}
&.new-alert {
animation: slideInFromRight 0.5s ease-out forwards;
.alert-card:nth-child(1) { animation-delay: 0.1s; }
.alert-card:nth-child(2) { animation-delay: 0.15s; }
.alert-card:nth-child(3) { animation-delay: 0.2s; }
.alert-card:nth-child(n+4) { animation-delay: 0.25s; }
[data-theme='dark'] {
:host {
.alert-card {
background-color: @common-background-color-dark;
.alert-header {
.alert-info {
.alert-meta-info {
color: rgb(214, 214, 214);
}
}
}
.alert-details {
::ng-deep {
.ant-collapse {
.ant-collapse-item {
.ant-collapse-header {
&:hover {
background-color: rgba(0, 0, 0, 0.05);
}
.ant-collapse-extra {
color: rgb(236, 236, 236);
}
}
}
}
}
.detail-section {
.section-title {
color: #cdcdcd;
}
.alert-count {
color: #ececec;
}
}
.alert-annotations {
.annotation-item {
.annotation-key {
color: #dfdede;
}
.annotation-value {
color: #e3e3e3;
}
}
}
.time-info {
.time-item {
.time-label {
color: #dfdede;
}
.time-value {
color: #e3e3e3;
}
}
}
}
}
}
}
@@ -54,8 +54,12 @@
groupStyle="width: 250px;"
[placeholder]="'alert.group-converge.name' | i18n"
[(value)]="search"
(valueChange)="onFilterChange()"
(keydown.enter)="onFilterChange()"
(cleared)="onFilterChange()"
/>
<button nz-button nzType="primary" (click)="onFilterChange()" class="mobile-hide">
{{ 'common.search' | i18n }}
</button>
</ng-template>
</app-toolbar>
@@ -77,12 +81,13 @@
<thead>
<tr>
<th nzAlign="center" nzLeft nzWidth="3%" [(nzChecked)]="checkedAll" (nzCheckedChange)="onAllChecked($event)"></th>
<th nzAlign="center" nzWidth="15%">{{ 'alert.group-converge.name' | i18n }}</th>
<th nzAlign="center" nzWidth="20%">{{ 'alert.group-converge.group-labels' | i18n }}</th>
<th nzAlign="center" nzWidth="14%">{{ 'alert.group-converge.name' | i18n }}</th>
<th nzAlign="center" nzWidth="12%">{{ 'alert.group-converge.group-labels' | i18n }}</th>
<th nzAlign="center" nzWidth="10%">{{ 'alert.group-converge.group-wait' | i18n }}</th>
<th nzAlign="center" nzWidth="10%">{{ 'alert.group-converge.group-interval' | i18n }}</th>
<th nzAlign="center" nzWidth="10%">{{ 'alert.group-converge.repeat-interval' | i18n }}</th>
<th nzAlign="center" nzWidth="8%" nzRight>{{ 'alert.group-converge.enable' | i18n }}</th>
<th nzAlign="center" nzWidth="10%" nzRight>{{ 'common.enable' | i18n }}</th>
<th nzAlign="center" nzWidth="14%">{{ 'common.edit-time' | i18n }}</th>
<th nzAlign="center" nzWidth="12%" nzRight>{{ 'common.edit' | i18n }}</th>
</tr>
</thead>
@@ -101,6 +106,7 @@
<td nzAlign="center" nzRight>
<nz-switch [(ngModel)]="data.enable" (ngModelChange)="updateGroupConverge(data)" name="enable"></nz-switch>
</td>
<td nzAlign="center">{{ (data.gmtUpdate ? data.gmtUpdate : data.gmtCreate) | date : 'YYYY-MM-dd HH:mm:ss' }}</td>
<td nzAlign="center" nzRight>
<div class="actions">
<button
@@ -54,8 +54,12 @@
groupStyle="width: 250px;"
[placeholder]="'alert.inhibit.name' | i18n"
[(value)]="search"
(valueChange)="onFilterChange()"
(keydown.enter)="onFilterChange()"
(cleared)="onFilterChange()"
/>
<button nz-button nzType="primary" (click)="onFilterChange()" class="mobile-hide">
{{ 'common.search' | i18n }}
</button>
</ng-template>
</app-toolbar>
@@ -78,10 +82,11 @@
<tr>
<th nzAlign="center" nzLeft nzWidth="3%" [(nzChecked)]="checkedAll" (nzCheckedChange)="onAllChecked($event)"></th>
<th nzAlign="center" nzWidth="15%">{{ 'alert.inhibit.name' | i18n }}</th>
<th nzAlign="center" nzWidth="20%">{{ 'alert.inhibit.source_labels' | i18n }}</th>
<th nzAlign="center" nzWidth="20%">{{ 'alert.inhibit.target_labels' | i18n }}</th>
<th nzAlign="center" nzWidth="18%">{{ 'alert.inhibit.source_labels' | i18n }}</th>
<th nzAlign="center" nzWidth="18%">{{ 'alert.inhibit.target_labels' | i18n }}</th>
<th nzAlign="center" nzWidth="15%">{{ 'alert.inhibit.equal_labels' | i18n }}</th>
<th nzAlign="center" nzWidth="8%" nzRight>{{ 'common.enable' | i18n }}</th>
<th nzAlign="center" nzWidth="12%" nzRight>{{ 'common.enable' | i18n }}</th>
<th nzAlign="center" nzWidth="14%">{{ 'common.edit-time' | i18n }}</th>
<th nzAlign="center" nzWidth="12%" nzRight>{{ 'common.edit' | i18n }}</th>
</tr>
</thead>
@@ -101,6 +106,7 @@
<td nzAlign="center" nzRight>
<nz-switch [(ngModel)]="data.enable" (ngModelChange)="updateInhibit(data)" name="enable"></nz-switch>
</td>
<td nzAlign="center">{{ (data.gmtUpdate ? data.gmtUpdate : data.gmtCreate) | date : 'YYYY-MM-dd HH:mm:ss' }}</td>
<td nzAlign="center" nzRight>
<div class="actions">
<button nz-button nzType="primary" (click)="editInhibit(data.id)" nz-tooltip [nzTooltipTitle]="'alert.inhibit.edit' | i18n">

Some files were not shown because too many files have changed in this diff Show More