Compare commits

..
Author SHA1 Message Date
aias00 c51236318e Merge branch 'master' into bugfix-app-hide 2025-02-13 10:20:14 +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
aias00 decc6598e4 Merge branch 'master' into bugfix-app-hide 2025-02-13 10:19:43 +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
Logic 11ae95bd2f refactor(web-app): update app menu visibility on monitor creation
- Add updateLocalAppState method to update app menu visibility
- Call updateLocalAppState when creating a new monitor
- Move success notification and routing inside configuration update subscription
2025-02-12 22:28:42 +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
7dbcaaeda8 [alert] support multi query expr threshold (#3054)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
Signed-off-by: Logic <zqr10159@126.com>
Co-authored-by: Logic <zqr10159@dromara.org>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-02-06 21:33:56 +08:00
Logicandgithub-actions[bot] 4ddf0a9f3f [alert] support sse for alert center, no need to manually refresh the page, add slide-in animation (#3051)
Signed-off-by: Logic <zqr10159@126.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-02-06 16:13:01 +08:00
103 changed files with 4707 additions and 762 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,25 @@
/*
* 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;
/**
* Alibaba Cloud SMS properties
*/
public class AlibabaSmsProperties {
// todo add properties
}
@@ -0,0 +1,52 @@
/*
* 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;
}
@@ -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;
}
@@ -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,135 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.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.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;
/**
* 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 ALIBABA:
// TODO: implement Alibaba SMS client
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);
}
}
@@ -17,18 +17,21 @@
package org.apache.hertzbeat.alert.service.impl;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Stack;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.service.DataSourceService;
import org.apache.hertzbeat.warehouse.db.QueryExecutor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.apache.commons.lang3.tuple.Pair;
import java.util.List;
import java.util.Map;
import java.util.ArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.util.StringUtils;
/**
* datasource service
@@ -40,10 +43,15 @@ public class DataSourceServiceImpl implements DataSourceService {
@Autowired(required = false)
private List<QueryExecutor> executors;
private static final Pattern EXPR_PATTERN = Pattern.compile("^(.*?)([><]=?|==|!=)\\s*(\\d+(\\.\\d+)?)$");
private static final Pattern EXPR_TOKEN = Pattern.compile("\\(|\\)|[a-zA-Z_][a-zA-Z0-9_={}\\[\\]\".]*|\\d+(\\.\\d+)?|>=|<=|==|!=|>|<|and|or|unless");
private static final String THRESHOLD = "__threshold__";
private static final String VALUE = "__value__";
@Override
public List<Map<String, Object>> calculate(String datasource, String expr) {
if (!StringUtils.hasText(expr)) {
throw new IllegalArgumentException("Empty expression");
}
if (executors == null || executors.isEmpty()) {
throw new IllegalArgumentException("No query executor found");
}
@@ -51,52 +59,206 @@ public class DataSourceServiceImpl implements DataSourceService {
if (executor == null) {
throw new IllegalArgumentException("Unsupported datasource: " + datasource);
}
// todo support multiple expr: and or logic operation
Pair<String, Pair<String, Double>> parsedExpr = parseExpression(expr);
String query = parsedExpr.getLeft();
String operator = parsedExpr.getRight().getLeft();
Double threshold = parsedExpr.getRight().getRight();
// replace all white space
expr = expr.replaceAll("\\s+", " ");
try {
List<Map<String, Object>> results = executor.execute(query);
return evaluateResults(results, operator, threshold);
return evaluate(expr, executor);
} catch (Exception e) {
log.error("Error executing query on datasource {}: {}", datasource, e.getMessage());
throw new RuntimeException("Query execution failed", e);
}
}
private Pair<String, Pair<String, Double>> parseExpression(String expr) {
Matcher matcher = EXPR_PATTERN.matcher(expr.trim());
if (matcher.find()) {
// promql, sql expr
String query = matcher.group(1).trim();
// operator
String operator = matcher.group(2).trim();
// value
Double threshold = Double.valueOf(matcher.group(3).trim());
return Pair.of(query, Pair.of(operator, threshold));
} else {
throw new IllegalArgumentException("Invalid expression format: " + expr);
private List<Map<String, Object>> evaluate(String expr, QueryExecutor executor) {
Stack<List<Map<String, Object>>> values = new Stack<>();
Stack<String> operators = new Stack<>();
Matcher matcher = EXPR_TOKEN.matcher(expr);
List<String> tokens = new ArrayList<>();
while (matcher.find()) {
tokens.add(matcher.group());
}
}
private List<Map<String, Object>> evaluateResults(List<Map<String, Object>> results, String operator, Double threshold) {
List<Map<String, Object>> filteredResults = new ArrayList<>();
for (Map<String, Object> result : results) {
Object values = result.get("__value__");
if (values == null) {
// ignore the query result data is empty
continue;
for (String token : tokens) {
if (token.equals("(")) {
operators.push(token);
} else if (token.equals(")")) {
while (!operators.isEmpty() && !operators.peek().equals("(")) {
applyOperator(values, operators.pop());
}
// remove the left parenthesis
operators.pop();
} else if (token.matches(">=|<=|==|!=|>|<")) {
operators.push(token);
} else if (token.equals("and") || token.equals("or") || token.equals("unless")) {
while (!operators.isEmpty() && precedence(operators.peek()) >= precedence(token)) {
applyOperator(values, operators.pop());
}
operators.push(token);
} else if (token.matches("\\d+(\\.\\d+)?")) {
double value = Double.parseDouble(token);
List<Map<String, Object>> numAsList = new ArrayList<>();
numAsList.add(Map.of(THRESHOLD, value));
values.push(numAsList);
} else if (token.matches("[a-zA-Z_][a-zA-Z0-9_={}\\[\\]\".]*")) {
List<Map<String, Object>> results = executor.execute(token);
values.push(results);
}
// values may be a list of values, or a single value
Object matchValue = evaluateCondition(values, operator, threshold);
result.put("__value__", matchValue);
// if matchValue is null, mean not match the threshold
// if not null, mean match the threshold
filteredResults.add(result);
}
return filteredResults;
while (!operators.isEmpty()) {
applyOperator(values, operators.pop());
}
return values.isEmpty() ? new LinkedList<>() : values.pop();
}
private int precedence(String op) {
return switch (op) {
case "or" -> 1;
case "unless" -> 2;
case "and" -> 3;
case ">", "<", ">=", "<=", "==", "!=" -> 4;
default -> 0;
};
}
private void applyOperator(Stack<List<Map<String, Object>>> values, String op) {
if (values.size() < 2) {
return;
};
List<Map<String, Object>> rightOperand = values.pop();
List<Map<String, Object>> leftOperand = values.pop();
if (rightOperand.size() == 1 && rightOperand.get(0).containsKey(THRESHOLD)) {
double threshold = (double) rightOperand.get(0).get(THRESHOLD);
List<Map<String, Object>> result = new ArrayList<>();
for (Map<String, Object> item : leftOperand) {
Object queryValues = item.get(VALUE);
if (queryValues == null) {
// ignore the query result data is empty
continue;
}
// queryValues may be a list of values, or a single value
Object matchValue = evaluateCondition(queryValues, op, threshold);
item.put(VALUE, matchValue);
// if matchValue is null, mean not match the threshold
// if not null, mean match the threshold
result.add(new HashMap<>(item));
}
if (!result.isEmpty()) {
values.push(result);
}
return;
}
Map<String, Object> leftMap = null;
boolean leftMatch = false;
Map<String, Object> rightMap = null;
boolean rightMatch = false;
switch (op) {
case "and" -> {
for (Map<String, Object> item : leftOperand) {
if (leftMap == null) {
leftMap = item;
}
if (item.get(VALUE) != null) {
leftMap = item;
leftMatch = true;
break;
}
}
for (Map<String, Object> item : rightOperand) {
if (rightMap == null) {
rightMap = item;
}
if (item.get(VALUE) != null) {
rightMap = item;
rightMatch = true;
break;
}
}
if (leftMatch && rightMatch) {
rightMap.putAll(leftMap);
values.push(new LinkedList<>(List.of(rightMap)));
} else if (leftMap != null) {
leftMap.put(VALUE, null);
values.push(new LinkedList<>(List.of(leftMap)));
} else if (rightMap != null) {
rightMap.put(VALUE, null);
values.push(new LinkedList<>(List.of(rightMap)));
}
}
case "or" -> {
for (Map<String, Object> item : leftOperand) {
if (leftMap == null) {
leftMap = item;
}
if (item.get(VALUE) != null) {
leftMap = item;
leftMatch = true;
break;
}
}
for (Map<String, Object> item : rightOperand) {
if (rightMap == null) {
rightMap = item;
}
if (item.get(VALUE) != null) {
rightMap = item;
rightMatch = true;
break;
}
}
if (leftMatch && rightMatch) {
rightMap.putAll(leftMap);
values.push(new LinkedList<>(List.of(rightMap)));
} else if (leftMatch) {
values.push(new LinkedList<>(List.of(leftMap)));
} else if (rightMatch) {
values.push(new LinkedList<>(List.of(rightMap)));
} else {
if (leftMap != null && rightMap != null) {
rightMap.putAll(leftMap);
values.push(new LinkedList<>(List.of(rightMap)));
} else if (leftMap != null) {
values.push(new LinkedList<>(List.of(leftMap)));
} else if (rightMap != null){
values.push(new LinkedList<>(List.of(rightMap)));
}
}
}
case "unless" -> {
for (Map<String, Object> item : leftOperand) {
if (leftMap == null) {
leftMap = item;
}
if (item.get(VALUE) != null) {
leftMap = item;
leftMatch = true;
break;
}
}
for (Map<String, Object> item : rightOperand) {
if (rightMap == null) {
rightMap = item;
}
if (item.get(VALUE) != null) {
rightMap = item;
rightMatch = true;
break;
}
}
if (leftMatch && !rightMatch) {
values.push(new LinkedList<>(List.of(leftMap)));
} else {
if (leftMap != null) {
leftMap.put(VALUE, null);
values.push(new LinkedList<>(List.of(leftMap)));
} else {
if (rightMap != null) {
rightMap.put(VALUE, null);
values.push(new LinkedList<>(List.of(rightMap)));
}
}
}
}
default -> throw new IllegalArgumentException("Unsupported operator: " + op);
}
}
private Object evaluateCondition(Object value, String operator, Double threshold) {
@@ -178,4 +340,8 @@ public class DataSourceServiceImpl implements DataSourceService {
return null;
}
}
public void setExecutors(List<QueryExecutor> mockExecutor) {
this.executors = mockExecutor;
}
}
@@ -0,0 +1,181 @@
/*
* 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.TencentCloudApiSignV3;
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 java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
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 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 = TencentCloudApiSignV3.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;
}
}
@@ -0,0 +1,84 @@
/*
* 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 java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
/**
* used to calculate the signature of the Tencent Cloud API.
*/
public class TencentCloudApiSignV3 {
private static final Charset UTF8 = StandardCharsets.UTF_8;
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 = 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 = sha256Hex(canonicalRequest);
String stringToSign = algorithm + "\n" + timestamp + "\n" + credentialScope + "\n" + hashedCanonicalRequest;
// Step 3: Calculate the signature
byte[] secretDate = hmac256(("TC3" + secretKey).getBytes(UTF8), date);
byte[] secretService = hmac256(secretDate, service);
byte[] secretSigning = hmac256(secretService, "tc3_request");
String signature = DatatypeConverter.printHexBinary(hmac256(secretSigning, stringToSign)).toLowerCase();
// Step 4: Construct the Authorization header
return algorithm + " " + "Credential=" + secretId + "/" + credentialScope + ", "
+ "SignedHeaders=" + signedHeaders + ", " + "Signature=" + signature;
}
public static byte[] hmac256(byte[] key, String msg) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKeySpec = new SecretKeySpec(key, mac.getAlgorithm());
mac.init(secretKeySpec);
return mac.doFinal(msg.getBytes(UTF8));
}
public static String sha256Hex(String s) throws Exception {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] d = md.digest(s.getBytes(UTF8));
return DatatypeConverter.printHexBinary(d).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);
@@ -0,0 +1,547 @@
/*
* 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.
*/
/*
* 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 static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import java.util.HashMap;
import org.apache.hertzbeat.alert.service.impl.DataSourceServiceImpl;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Map;
import org.mockito.Mockito;
import org.apache.hertzbeat.warehouse.db.QueryExecutor;
/**
* test case for {@link DataSourceService}
*/
class DataSourceServiceTest {
private DataSourceServiceImpl dataSourceService;
@BeforeEach
void setUp() {
dataSourceService = new DataSourceServiceImpl();
}
@Test
void calculate1() {
List<Map<String, Object>> prometheusData = List.of(
new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")),
new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2"))
);
QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
Mockito.when(mockExecutor.support("promql")).thenReturn(true);
Mockito.when(mockExecutor.execute(Mockito.anyString())).thenReturn(prometheusData);
dataSourceService.setExecutors(List.of(mockExecutor));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total > 150");
assertEquals(2, result.size());
assertNull(result.get(0).get("__value__"));
assertEquals(200.0, result.get(1).get("__value__"));
}
@Test
void calculate2() {
List<Map<String, Object>> prometheusData = List.of(
new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")),
new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2"))
);
QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
Mockito.when(mockExecutor.support("promql")).thenReturn(true);
Mockito.when(mockExecutor.execute(Mockito.anyString())).thenReturn(prometheusData);
dataSourceService.setExecutors(List.of(mockExecutor));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total <= 100");
assertEquals(2, result.size());
assertEquals(100.0, result.get(0).get("__value__"));
assertNull(result.get(1).get("__value__"));
}
@Test
void calculate3() {
List<Map<String, Object>> prometheusData = List.of(
new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")),
new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2"))
);
QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
Mockito.when(mockExecutor.support("promql")).thenReturn(true);
Mockito.when(mockExecutor.execute(Mockito.anyString())).thenReturn(prometheusData);
dataSourceService.setExecutors(List.of(mockExecutor));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total >= 200");
assertEquals(2, result.size());
assertNull(result.get(0).get("__value__"));
assertEquals(200.0, result.get(1).get("__value__"));
}
@Test
void calculate4() {
List<Map<String, Object>> prometheusData = List.of(
new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")),
new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2"))
);
QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
Mockito.when(mockExecutor.support("promql")).thenReturn(true);
Mockito.when(mockExecutor.execute(Mockito.anyString())).thenReturn(prometheusData);
dataSourceService.setExecutors(List.of(mockExecutor));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total > 250");
assertEquals(2, result.size());
assertNull(result.get(0).get("__value__"));
assertNull(result.get(1).get("__value__"));
}
@Test
void calculate5() {
List<Map<String, Object>> prometheusData = List.of(
new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")),
new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2"))
);
QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
Mockito.when(mockExecutor.support("promql")).thenReturn(true);
Mockito.when(mockExecutor.execute(Mockito.anyString())).thenReturn(prometheusData);
dataSourceService.setExecutors(List.of(mockExecutor));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total < 100");
assertEquals(2, result.size());
assertNull(result.get(0).get("__value__"));
assertNull(result.get(1).get("__value__"));
}
@Test
void calculate6() {
List<Map<String, Object>> prometheusData = List.of(
new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")),
new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2"))
);
QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
Mockito.when(mockExecutor.support("promql")).thenReturn(true);
Mockito.when(mockExecutor.execute(Mockito.anyString())).thenReturn(prometheusData);
dataSourceService.setExecutors(List.of(mockExecutor));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total > 200");
assertEquals(2, result.size());
assertNull(result.get(0).get("__value__"));
assertNull(result.get(1).get("__value__"));
}
@Test
void calculate7() {
List<Map<String, Object>> prometheusData = List.of(
new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")),
new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2"))
);
QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
Mockito.when(mockExecutor.support("promql")).thenReturn(true);
Mockito.when(mockExecutor.execute(Mockito.anyString())).thenReturn(prometheusData);
dataSourceService.setExecutors(List.of(mockExecutor));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "(node_cpu_seconds_total <= 100)");
assertEquals(2, result.size());
assertEquals(100.0, result.get(0).get("__value__"));
assertNull(result.get(1).get("__value__"));
}
@Test
void calculate8() {
List<Map<String, Object>> prometheusData = List.of(
new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")),
new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2"))
);
QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
Mockito.when(mockExecutor.support("promql")).thenReturn(true);
Mockito.when(mockExecutor.execute(Mockito.anyString())).thenReturn(prometheusData);
dataSourceService.setExecutors(List.of(mockExecutor));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total{mode=\"user\"}[4m] <= 100");
assertEquals(2, result.size());
assertEquals(100.0, result.get(0).get("__value__"));
assertNull(result.get(1).get("__value__"));
}
@Test
void calculate9() {
List<Map<String, Object>> prometheusData = List.of(
new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")),
new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2"))
);
QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
Mockito.when(mockExecutor.support("promql")).thenReturn(true);
Mockito.when(mockExecutor.execute(Mockito.anyString())).thenReturn(prometheusData);
dataSourceService.setExecutors(List.of(mockExecutor));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total{mode=\"user\"} == 100");
assertEquals(2, result.size());
assertEquals(100.0, result.get(0).get("__value__"));
assertNull(result.get(1).get("__value__"));
}
@Test
void calculate10() {
List<Map<String, Object>> prometheusData = List.of(
new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")),
new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2"))
);
QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
Mockito.when(mockExecutor.support("promql")).thenReturn(true);
Mockito.when(mockExecutor.execute(Mockito.anyString())).thenReturn(prometheusData);
dataSourceService.setExecutors(List.of(mockExecutor));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total{mode=\"user\"} != 100");
assertEquals(2, result.size());
assertNull(result.get(0).get("__value__"));
assertEquals(200.0, result.get(1).get("__value__"));
}
@Test
void calculate11() {
List<Map<String, Object>> prometheusData = List.of(
new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")),
new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2"))
);
QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
Mockito.when(mockExecutor.support("promql")).thenReturn(true);
Mockito.when(mockExecutor.execute(Mockito.anyString())).thenReturn(prometheusData);
dataSourceService.setExecutors(List.of(mockExecutor));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total{mode=\"user\"} > 50 and node_cpu_seconds_total{mode=\"idle\"} < 120");
assertEquals(1, result.size());
assertEquals(100.0, result.get(0).get("__value__"));
}
@Test
void calculate12() {
List<Map<String, Object>> prometheusData = List.of(
new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")),
new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2"))
);
QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
Mockito.when(mockExecutor.support("promql")).thenReturn(true);
Mockito.when(mockExecutor.execute(Mockito.anyString())).thenReturn(prometheusData);
dataSourceService.setExecutors(List.of(mockExecutor));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "(node_cpu_seconds_total{mode=\"user\"} > 50) and (node_cpu_seconds_total{mode=\"idle\"} < 120)");
assertEquals(1, result.size());
assertEquals(100.0, result.get(0).get("__value__"));
}
@Test
void calculate13() {
List<Map<String, Object>> prometheusData = List.of(
new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")),
new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2"))
);
QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
Mockito.when(mockExecutor.support("promql")).thenReturn(true);
Mockito.when(mockExecutor.execute(Mockito.anyString())).thenReturn(prometheusData);
dataSourceService.setExecutors(List.of(mockExecutor));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total{mode=\"user\"} > 150 and node_cpu_seconds_total{mode=\"idle\"} < 220");
assertEquals(1, result.size());
assertEquals(200.0, result.get(0).get("__value__"));
}
@Test
void calculate14() {
List<Map<String, Object>> prometheusData = List.of(
new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")),
new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2"))
);
QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
Mockito.when(mockExecutor.support("promql")).thenReturn(true);
Mockito.when(mockExecutor.execute(Mockito.anyString())).thenReturn(prometheusData);
dataSourceService.setExecutors(List.of(mockExecutor));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "(node_cpu_seconds_total{mode=\"user\"} > 150) and (node_cpu_seconds_total{mode=\"idle\"} < 220)");
assertEquals(1, result.size());
assertEquals(200.0, result.get(0).get("__value__"));
}
@Test
void calculate15() {
List<Map<String, Object>> prometheusData1 = List.of(
new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")),
new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2"))
);
List<Map<String, Object>> prometheusData2 = List.of(
new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")),
new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2"))
);
QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
Mockito.when(mockExecutor.support("promql")).thenReturn(true);
Mockito.when(mockExecutor.execute("node_cpu_seconds_total{mode=\"user\"}")).thenReturn(prometheusData1);
Mockito.when(mockExecutor.execute("node_cpu_seconds_total{mode=\"idle\"}")).thenReturn(prometheusData2);
dataSourceService.setExecutors(List.of(mockExecutor));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total{mode=\"user\"} > 250 and node_cpu_seconds_total{mode=\"idle\"} < 220");
assertEquals(1, result.size());
assertNull(result.get(0).get("__value__"));
}
@Test
void calculate16() {
List<Map<String, Object>> prometheusData1 = List.of(
new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")),
new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2"))
);
List<Map<String, Object>> prometheusData2 = List.of(
new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")),
new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2"))
);
QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
Mockito.when(mockExecutor.support("promql")).thenReturn(true);
Mockito.when(mockExecutor.execute("node_cpu_seconds_total{mode=\"user\"}")).thenReturn(prometheusData1);
Mockito.when(mockExecutor.execute("node_cpu_seconds_total{mode=\"idle\"}")).thenReturn(prometheusData2);
dataSourceService.setExecutors(List.of(mockExecutor));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total{mode=\"user\"} > 50 and node_cpu_seconds_total{mode=\"idle\"} < 20");
assertEquals(1, result.size());
assertNull(result.get(0).get("__value__"));
}
@Test
void calculate17() {
List<Map<String, Object>> prometheusData1 = List.of(
new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")),
new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2"))
);
List<Map<String, Object>> prometheusData2 = List.of(
new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")),
new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2"))
);
QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
Mockito.when(mockExecutor.support("promql")).thenReturn(true);
Mockito.when(mockExecutor.execute("node_cpu_seconds_total{mode=\"user\"}")).thenReturn(prometheusData1);
Mockito.when(mockExecutor.execute("node_cpu_seconds_total{mode=\"idle\"}")).thenReturn(prometheusData2);
dataSourceService.setExecutors(List.of(mockExecutor));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total{mode=\"user\"} > 150 or node_cpu_seconds_total{mode=\"idle\"} < 20");
assertEquals(1, result.size());
assertEquals(200.0, result.get(0).get("__value__"));
}
@Test
void calculate18() {
List<Map<String, Object>> prometheusData1 = List.of(
new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")),
new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2"))
);
List<Map<String, Object>> prometheusData2 = List.of(
new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")),
new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2"))
);
QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
Mockito.when(mockExecutor.support("promql")).thenReturn(true);
Mockito.when(mockExecutor.execute("node_cpu_seconds_total{mode=\"user\"}")).thenReturn(prometheusData1);
Mockito.when(mockExecutor.execute("node_cpu_seconds_total{mode=\"idle\"}")).thenReturn(prometheusData2);
dataSourceService.setExecutors(List.of(mockExecutor));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total{mode=\"user\"} > 250 or node_cpu_seconds_total{mode=\"idle\"} < 120");
assertEquals(1, result.size());
assertEquals(100.0, result.get(0).get("__value__"));
}
@Test
void calculate19() {
List<Map<String, Object>> prometheusData1 = List.of(
new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")),
new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2"))
);
List<Map<String, Object>> prometheusData2 = List.of(
new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")),
new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2"))
);
QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
Mockito.when(mockExecutor.support("promql")).thenReturn(true);
Mockito.when(mockExecutor.execute("node_cpu_seconds_total{mode=\"user\"}")).thenReturn(prometheusData1);
Mockito.when(mockExecutor.execute("node_cpu_seconds_total{mode=\"idle\"}")).thenReturn(prometheusData2);
dataSourceService.setExecutors(List.of(mockExecutor));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total{mode=\"user\"} > 250 or node_cpu_seconds_total{mode=\"idle\"} < 20");
assertEquals(1, result.size());
assertNull(result.get(0).get("__value__"));
}
@Test
void calculate20() {
List<Map<String, Object>> prometheusData1 = List.of(
new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")),
new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2"))
);
List<Map<String, Object>> prometheusData2 = List.of(
new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "key", "node1")),
new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "book", "node2"))
);
QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
Mockito.when(mockExecutor.support("promql")).thenReturn(true);
Mockito.when(mockExecutor.execute("node_cpu_seconds_total{mode=\"user\"}")).thenReturn(prometheusData1);
Mockito.when(mockExecutor.execute("node_cpu_seconds_total{mode=\"idle\"}")).thenReturn(prometheusData2);
dataSourceService.setExecutors(List.of(mockExecutor));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total{mode=\"user\"} > 50 or node_cpu_seconds_total{mode=\"idle\"} < 320");
assertEquals(1, result.size());
assertNotNull(result.get(0).get("__value__"));
}
@Test
void calculate21() {
List<Map<String, Object>> prometheusData1 = List.of(
new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")),
new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2"))
);
List<Map<String, Object>> prometheusData2 = List.of(
new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "key", "node1")),
new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "book", "node2"))
);
QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
Mockito.when(mockExecutor.support("promql")).thenReturn(true);
Mockito.when(mockExecutor.execute("node_cpu_seconds_total{mode=\"user\"}")).thenReturn(prometheusData1);
Mockito.when(mockExecutor.execute("node_cpu_seconds_total{mode=\"idle\"}")).thenReturn(prometheusData2);
dataSourceService.setExecutors(List.of(mockExecutor));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total{mode=\"user\"} > 50 unless node_cpu_seconds_total{mode=\"idle\"} < 320");
assertEquals(1, result.size());
assertNull(result.get(0).get("__value__"));
}
@Test
void calculate22() {
List<Map<String, Object>> prometheusData1 = List.of(
new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")),
new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2"))
);
List<Map<String, Object>> prometheusData2 = List.of(
new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "key", "node1")),
new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "book", "node2"))
);
QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
Mockito.when(mockExecutor.support("promql")).thenReturn(true);
Mockito.when(mockExecutor.execute("node_cpu_seconds_total{mode=\"user\"}")).thenReturn(prometheusData1);
Mockito.when(mockExecutor.execute("node_cpu_seconds_total{mode=\"idle\"}")).thenReturn(prometheusData2);
dataSourceService.setExecutors(List.of(mockExecutor));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total{mode=\"user\"} > 50 unless node_cpu_seconds_total{mode=\"idle\"} < 20");
assertEquals(1, result.size());
assertNotNull(result.get(0).get("__value__"));
}
@Test
void calculate23() {
List<Map<String, Object>> prometheusData1 = List.of(
new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")),
new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2"))
);
List<Map<String, Object>> prometheusData2 = List.of(
new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "key", "node1")),
new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "book", "node2"))
);
QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
Mockito.when(mockExecutor.support("promql")).thenReturn(true);
Mockito.when(mockExecutor.execute("node_cpu_seconds_total{mode=\"user\"}")).thenReturn(prometheusData1);
Mockito.when(mockExecutor.execute("node_cpu_seconds_total{mode=\"idle\"}")).thenReturn(prometheusData2);
dataSourceService.setExecutors(List.of(mockExecutor));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total{mode=\"user\"} > 250 unless node_cpu_seconds_total{mode=\"idle\"} < 20");
assertEquals(1, result.size());
assertNull(result.get(0).get("__value__"));
}
@Test
void calculate24() {
List<Map<String, Object>> prometheusData = List.of();
QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
Mockito.when(mockExecutor.support("promql")).thenReturn(true);
Mockito.when(mockExecutor.execute(Mockito.anyString())).thenReturn(prometheusData);
dataSourceService.setExecutors(List.of(mockExecutor));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total > 150");
assertEquals(0, result.size());
}
@Test
void calculate25() {
List<Map<String, Object>> prometheusData = List.of();
QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
Mockito.when(mockExecutor.support("promql")).thenReturn(true);
Mockito.when(mockExecutor.execute(Mockito.anyString())).thenReturn(prometheusData);
dataSourceService.setExecutors(List.of(mockExecutor));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total{mode=\"user\"} > 250 unless node_cpu_seconds_total{mode=\"idle\"} < 20");
assertEquals(0, result.size());
}
@Test
void calculate26() {
List<Map<String, Object>> prometheusData = List.of(
new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2"))
);
QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
Mockito.when(mockExecutor.support("promql")).thenReturn(true);
Mockito.when(mockExecutor.execute(Mockito.anyString())).thenReturn(prometheusData);
dataSourceService.setExecutors(List.of(mockExecutor));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total > 150");
assertEquals(1, result.size());
assertEquals(200.0, result.get(0).get("__value__"));
}
}
@@ -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;
+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,28 @@
/*
* 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";
}
@@ -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;
@@ -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,17 @@ alerter:
# alert inhibit ttl unit ms, default 14400000(4 hours)
inhibit:
ttl: 14400000
sms:
enable: true
type: tencent
tencent:
secret-id:
secret-key:
app-id:
sign-name:
template-id:
alibaba:
app-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^_^
@@ -14,7 +14,7 @@
# limitations under the License.
# The monitoring type categoryservice-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring
category: server
category: llm
# The monitoring type eg: linux windows tomcat mysql aws...
app: nvidia
# The monitoring i18n name
@@ -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);
}
}
}
}
+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>
-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"/>
@@ -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
@@ -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
@@ -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
@@ -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'));
}
});
}
}
@@ -207,7 +207,6 @@ export class HeaderNotifyComponent implements OnInit, OnDestroy {
let item = {
id: alert.id,
avatar: '/assets/img/notification.svg',
// title: `${alert.tags?.monitorName}--${this.i18nSvc.fanyi(`alert.severity.${alert.severity}`)}`,
title: alert.content,
datetime: new Date(alert.activeAt).toLocaleString(),
color: 'blue',
@@ -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
];
@@ -1,3 +1,5 @@
@import "~src/styles/theme";
:host ::ng-deep app-toolbar {
.center-content {
display: flex;
@@ -67,17 +69,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 +114,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 +134,7 @@
display: flex;
gap: 16px;
margin-top: 8px;
color: #8c8c8c;
color: rgba(0, 0, 0, 0.5);
font-size: 12px;
i {
@@ -142,16 +167,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 +192,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 +205,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 +302,94 @@
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;
}
}
}
@@ -1,20 +1,23 @@
@import "~src/styles/theme";
.alert-integration-container {
display: flex;
height: 100%;
background: #fff;
background: @common-background-color;
border-radius: 4px;
.data-sources {
width: 240px;
border-right: 1px solid #f0f0f0;
padding: 16px;
background: @common-background-color;
h2 {
margin-bottom: 16px;
font-size: 16px;
font-weight: 500;
}
.source-list {
.source-item {
display: flex;
@@ -23,33 +26,30 @@
cursor: pointer;
border-radius: 4px;
transition: all 0.3s;
&:hover {
background: #f5f5f5;
background: rgba(0, 0, 0, 0.05);
}
&.active {
background: #e6f7ff;
background: rgba(0, 0, 0, 0.1);
}
img {
width: 24px;
height: 24px;
margin-right: 8px;
}
span {
color: #333;
}
}
}
}
.doc-content {
flex: 1;
padding: 24px;
overflow-y: auto;
background: @common-background-color;
h2 {
margin-bottom: 24px;
font-size: 20px;
@@ -57,3 +57,28 @@
}
}
}
[data-theme='dark'] {
:host {
.alert-integration-container {
background: @common-background-color-dark;
.data-sources {
background: @common-background-color-dark;
}
.doc-content {
background: @common-background-color-dark;
}
.source-list {
.source-item {
&:hover {
background: rgba(255, 255, 255, 0.4);
}
&.active {
background: rgba(255, 255, 255, 0.6);
}
}
}
}
}
}
@@ -306,7 +306,7 @@
[delay]="300"
[zoomOnHover]="{ scale: 1.4, transitionTime: 0.6, delay: 0.4 }"
[overflow]="false"
[background]="'white no-repeat fixed center'"
[background]="theme == 'dark' ? '#141414' : 'white'"
>
</angular-tag-cloud>
</nz-spin>
@@ -1,4 +1,3 @@
@import '@delon/theme/index';
:host ::ng-deep {
.ant-timeline {
.ant-timeline-label {
@@ -34,6 +34,7 @@ import { AlertService } from '../../service/alert.service';
import { CollectorService } from '../../service/collector.service';
import { MonitorService } from '../../service/monitor.service';
import { TagService } from '../../service/tag.service';
import { ThemeService } from '../../service/theme.service';
import { formatTagName } from '../../shared/utils/common-util';
@Component({
@@ -51,9 +52,11 @@ export class DashboardComponent implements OnInit, OnDestroy {
private collectorSvc: CollectorService,
@Inject(ALAIN_I18N_TOKEN) private i18nSvc: I18NService,
private router: Router,
private themeSvc: ThemeService,
private cdr: ChangeDetectorRef
) {}
theme: string = 'default';
// Tag Word Cloud
wordCloudData: CloudData[] = [];
wordCloudDataLoading: boolean = false;
@@ -178,6 +181,7 @@ export class DashboardComponent implements OnInit, OnDestroy {
alertContentLoading: boolean = false;
ngOnInit(): void {
this.theme = this.themeSvc.getTheme() || 'default';
this.appsCountTheme = {
title: {
text: `{a|${this.i18nSvc.fanyi('dashboard.monitors.title')}}`,
@@ -75,18 +75,6 @@ export class MonitorDataChartComponent implements OnInit {
show: true,
orient: 'vertical',
feature: {
dataZoom: {
yAxisIndex: 'none',
title: {
zoom: this.i18nSvc.fanyi('monitor.detail.chart.zoom'),
back: this.i18nSvc.fanyi('monitor.detail.chart.back')
},
emphasis: {
iconStyle: {
textPosition: 'left'
}
}
},
saveAsImage: {
title: this.i18nSvc.fanyi('monitor.detail.chart.save'),
emphasis: {
@@ -223,7 +211,10 @@ export class MonitorDataChartComponent implements OnInit {
{
type: 'inside',
start: 0,
end: 100
end: 100,
zoomOnMouseWheel: false,
moveOnMouseMove: false,
moveOnMouseWheel: false
}
]
};
@@ -150,17 +150,32 @@ export class MonitorFormComponent implements OnChanges {
}
onParamBooleanChanged(booleanValue: boolean, field: string) {
// For SSL port linkage, port 80 by default is not enabled, but port 443 by default is enabled
if (field === 'ssl') {
const portParam = this.params.find(param => param.field === 'port');
if (portParam) {
if (booleanValue && (portParam.paramValue == null || parseInt(portParam.paramValue) === 80)) {
portParam.paramValue = 443;
this.notifySvc.info(this.i18nSvc.fanyi('common.notice'), this.i18nSvc.fanyi('monitor.new.notify.change-to-https'));
if (this.monitor.app === 'api') {
if (field === 'ssl') {
const portParam = this.params.find(param => param.field === 'port');
if (portParam) {
if (booleanValue && (portParam.paramValue == null || parseInt(portParam.paramValue) === 80)) {
portParam.paramValue = 443;
this.notifySvc.info(this.i18nSvc.fanyi('common.notice'), this.i18nSvc.fanyi('monitor.new.notify.change-to-https'));
}
if (!booleanValue && (portParam.paramValue == null || parseInt(portParam.paramValue) === 443)) {
portParam.paramValue = 80;
this.notifySvc.info(this.i18nSvc.fanyi('common.notice'), this.i18nSvc.fanyi('monitor.new.notify.change-to-http'));
}
}
if (!booleanValue && (portParam.paramValue == null || parseInt(portParam.paramValue) === 443)) {
portParam.paramValue = 80;
this.notifySvc.info(this.i18nSvc.fanyi('common.notice'), this.i18nSvc.fanyi('monitor.new.notify.change-to-http'));
}
} else if (this.monitor.app === 'ftp') {
if (field === 'ssl') {
const portParam = this.params.find(param => param.field === 'port');
if (portParam) {
if (booleanValue && (portParam.paramValue == null || parseInt(portParam.paramValue) === 21)) {
portParam.paramValue = 22;
this.notifySvc.info(this.i18nSvc.fanyi('common.notice'), this.i18nSvc.fanyi('monitor.new.notify.change-to-sftp'));
}
if (!booleanValue && (portParam.paramValue == null || parseInt(portParam.paramValue) === 22)) {
portParam.paramValue = 21;
this.notifySvc.info(this.i18nSvc.fanyi('common.notice'), this.i18nSvc.fanyi('monitor.new.notify.change-to-ftp'));
}
}
}
}
@@ -120,7 +120,7 @@
<app-multi-func-input
groupStyle="width: 120px;"
class="mobile-hide"
[placeholder]="'monitor.search.tag' | i18n"
[placeholder]="'monitor.search.label' | i18n"
[(value)]="labels"
(valueChange)="onTagChanged()"
/>
@@ -19,7 +19,7 @@
import { Component, Inject, OnInit } from '@angular/core';
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
import { I18NService } from '@core';
import { I18NService, StartupService } from '@core';
import { ALAIN_I18N_TOKEN, TitleService } from '@delon/theme';
import { NzNotificationService } from 'ng-zorro-antd/notification';
import { switchMap } from 'rxjs/operators';
@@ -32,6 +32,7 @@ import { Param } from '../../../pojo/Param';
import { ParamDefine } from '../../../pojo/ParamDefine';
import { AppDefineService } from '../../../service/app-define.service';
import { CollectorService } from '../../../service/collector.service';
import { GeneralConfigService } from '../../../service/general-config.service';
import { MonitorService } from '../../../service/monitor.service';
import { generateReadableRandomString } from '../../../shared/utils/common-util';
@@ -59,6 +60,8 @@ export class MonitorNewComponent implements OnInit {
private route: ActivatedRoute,
private router: Router,
private notifySvc: NzNotificationService,
private configService: GeneralConfigService,
private startUpSvc: StartupService,
@Inject(ALAIN_I18N_TOKEN) private i18nSvc: I18NService,
private titleSvc: TitleService,
private collectorSvc: CollectorService
@@ -168,13 +171,19 @@ export class MonitorNewComponent implements OnInit {
};
this.spinningTip = 'Loading...';
this.isSpinning = true;
this.monitorSvc.newMonitor(addMonitor).subscribe(
message => {
this.isSpinning = false;
if (message.code === 0) {
this.notifySvc.success(this.i18nSvc.fanyi('monitor.new.success'), '');
this.router.navigateByUrl(`/monitors?app=${info.monitor.app}`);
this.configService.updateAppTemplateConfig({ hide: false }, info.monitor.app).subscribe(() => {
this.startUpSvc.loadConfigResourceViaHttp().subscribe(() => {
this.isSpinning = false;
this.notifySvc.success(this.i18nSvc.fanyi('monitor.new.success'), '');
this.router.navigateByUrl(`/monitors?app=${info.monitor.app}`);
});
});
} else {
this.isSpinning = false;
this.notifySvc.error(this.i18nSvc.fanyi('monitor.new.failed'), message.msg);
}
},
@@ -17,7 +17,7 @@
~ under the License.
-->
<div class="br-8" style="background-color: snow; padding: 20px; box-shadow: 7px 5px #b421cc">
<div class="br-8" style="background-color: rgb(198 189 189 / 39%); padding: 20px; box-shadow: 7px 5px #b421cc">
<form nz-form [formGroup]="form" (ngSubmit)="submit()" role="form">
<nz-tabset [nzAnimated]="false" class="tabs" (nzSelectChange)="switch($event)">
<nz-tab [nzTitle]="'app.login.tab-login-credentials' | i18n">
@@ -27,7 +27,7 @@
<nz-divider></nz-divider>
<nz-layout style="height: 100vh; overflow: hidden">
<nz-sider nzTheme="light" style="height: 100%; overflow: hidden" [nzTrigger]="null">
<nz-sider [nzTheme]="theme == 'dark' ? 'dark' : 'light'" style="height: 100%; overflow: hidden" [nzTrigger]="null">
<app-monitor-select-menu
[loading]="menuLoading"
[data]="appMenusArr"
@@ -107,7 +107,7 @@
[nzOriginalText]="originalCode"
[(ngModel)]="code"
[nzEditorMode]="'diff'"
[nzEditorOption]="{ language: 'yaml', theme: 'vs-dark', folding: true, automaticLayout: true }"
[nzEditorOption]="{ language: 'yaml', theme: 'vs', folding: true, automaticLayout: true }"
></nz-code-editor>
</div>
</nz-content>
@@ -28,6 +28,7 @@ import { finalize } from 'rxjs/operators';
import { AppDefineService } from '../../../service/app-define.service';
import { GeneralConfigService } from '../../../service/general-config.service';
import { ThemeService } from '../../../service/theme.service';
@Component({
selector: 'app-define',
@@ -44,6 +45,7 @@ export class DefineComponent implements OnInit {
private startUpSvc: StartupService,
private route: ActivatedRoute,
private router: Router,
private themeSvc: ThemeService,
@Inject(ALAIN_I18N_TOKEN) private i18nSvc: I18NService
) {}
@@ -54,7 +56,8 @@ export class DefineComponent implements OnInit {
loading = false;
code: string = '';
originalCode: string = '';
dark: boolean = true;
dark: boolean = false;
theme: string = 'default';
currentApp: any = null;
saveLoading = false;
deleteLoading = false;
@@ -66,6 +69,7 @@ export class DefineComponent implements OnInit {
this.loadAppDefineContent(this.currentApp);
}
});
this.theme = this.themeSvc.getTheme() || 'default';
this.loadMenus();
this.code = `${this.i18nSvc.fanyi('define.new.code')}\n\n\n\n\n`;
this.originalCode = this.i18nSvc.fanyi('define.new.code');
@@ -262,7 +266,7 @@ export class DefineComponent implements OnInit {
.subscribe(
message => {
if (message.code === 0) {
this.loadMenus();
this.updateLocalAppState(app, hide);
this.startUpSvc.loadConfigResourceViaHttp().subscribe(() => {});
this.notifySvc.success(this.i18nSvc.fanyi('common.notify.apply-success'), '');
} else {
@@ -275,6 +279,18 @@ export class DefineComponent implements OnInit {
);
}
private updateLocalAppState(app: string, hide: boolean): void {
this.appMenusArr.forEach(([category, menuData]) => {
if (menuData.child) {
menuData.child.forEach((item: any) => {
if (item.value === app) {
item.hide = hide;
}
});
}
});
}
renderCategoryName(category: string): string {
let label = this.i18nSvc.fanyi(`menu.monitor.${category}`);
if (label == `menu.monitor.${category}`) {
@@ -31,6 +31,7 @@
<nz-option [nzValue]="'en_US'" [nzLabel]="'settings.system-config.locale.en_US' | i18n"></nz-option>
<nz-option [nzValue]="'zh_CN'" [nzLabel]="'settings.system-config.locale.zh_CN' | i18n"></nz-option>
<nz-option [nzValue]="'zh_TW'" [nzLabel]="'settings.system-config.locale.zh_TW' | i18n"></nz-option>
<nz-option [nzValue]="'ja_JP'" [nzLabel]="'settings.system-config.locale.ja-JP' | i18n"></nz-option>
</nz-select>
</se>
<se [label]="'settings.system-config.timezone' | i18n" [error]="'validation.required' | i18n">
@@ -58,7 +58,7 @@ export class SystemConfigComponent implements OnInit {
if (message.code === 0) {
if (message.data) {
this.config = message.data;
this.changeTheme(this.config.theme); // update theme after config is loaded
this.config.theme = this.themeService.getTheme() || 'default';
} else {
this.config = new SystemConfig();
}
@@ -94,6 +94,7 @@ export class SystemConfigComponent implements OnInit {
this.i18nSvc.loadLangData(language).subscribe(res => {
this.i18nSvc.use(language, res);
this.settings.setLayout('lang', language);
this.themeService.setTheme(this.config.theme);
setTimeout(() => this.doc.location.reload());
});
} else {
@@ -105,8 +106,4 @@ export class SystemConfigComponent implements OnInit {
}
);
}
changeTheme(theme: string): void {
this.themeService.changeTheme(theme);
}
}
@@ -127,7 +127,7 @@
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-label [nzSpan]="7" nzFor="description">{{ 'tag.description' | i18n }}</nz-form-label>
<nz-form-label [nzSpan]="7" nzFor="description">{{ 'label.description' | i18n }}</nz-form-label>
<nz-form-control [nzSpan]="12" [nzErrorTip]="'validation.required' | i18n">
<input [(ngModel)]="tag.description" nz-input name="description" type="text" id="description" />
</nz-form-control>
@@ -1,10 +1,11 @@
@import "~src/styles/theme";
.tag-cards-container {
padding: 24px 0;
.tag-card {
transition: all 0.3s;
border-radius: 8px;
background: #fff;
&:hover {
transform: translateY(-2px);
@@ -23,8 +24,7 @@
.ant-card-actions {
border-radius: 0 0 8px 8px;
background: #fafafa;
border-top: 1px solid #f0f0f0;
border-top: 1px solid rgba(240, 240, 240, 0.5);
min-height: 32px;
> li {
@@ -34,7 +34,7 @@
padding: 4px 0;
&:hover {
color: #1890ff;
color: @primary-color;
}
i {
@@ -51,20 +51,8 @@
align-items: center;
justify-content: center;
margin-bottom: 8px;
.tag-name {
font-size: 18px;
padding: 4px 8px;
border-radius: 4px;
margin: 0;
font-weight: 500;
line-height: 1.4;
text-align: center;
}
}
.tag-description {
color: rgba(35, 34, 34, 0.65);
font-size: 12px;
margin: 0;
display: -webkit-box;
@@ -77,31 +65,27 @@
}
}
.tag-card {
:global {
.ant-card-body {
padding: 12px;
}
}
}
.tag-content {
min-height: 80px;
display: flex;
flex-direction: column;
}
.tag-header {
margin-bottom: 8px;
.tag-name {
font-size: 18px;
padding: 4px 8px;
border-radius: 4px;
margin: 0;
font-weight: 500;
line-height: 1.4;
text-align: center;
background: @common-background-color;
}
.tag-description {
font-size: 12px;
color: rgba(0, 0, 0, 0.65);
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
line-height: 1.5;
[data-theme='dark'] {
:host {
.tag-name {
background: @common-background-color-dark;
}
}
}
@@ -57,7 +57,8 @@ export class LocalStorageService {
return localStorage.getItem(AuthorizationConst) != null;
}
public clear() {
localStorage.clear();
public clearAuthorization() {
localStorage.removeItem(AuthorizationConst);
localStorage.removeItem(RefreshTokenConst);
}
}
+5 -8
View File
@@ -36,11 +36,10 @@ export class ThemeService {
return localStorage.getItem(this.themeKey);
}
clearTheme(): void {
localStorage.removeItem(this.themeKey);
}
changeTheme(theme: string): void {
changeTheme(theme: string | null): void {
if (theme == null) {
theme = this.getTheme();
}
const style = this.doc.createElement('link');
style.type = 'text/css';
style.rel = 'stylesheet';
@@ -57,9 +56,6 @@ export class ThemeService {
const compactDom = this.doc.getElementById('compact-theme');
if (compactDom) compactDom.remove();
this.clearTheme();
return;
}
@@ -73,5 +69,6 @@ export class ThemeService {
// add new theme
this.doc.body.appendChild(style);
this.doc.body.setAttribute('data-theme', theme);
}
}
+12 -3
View File
@@ -270,8 +270,8 @@
"alert.setting.target.system_value_row_count": "Value rows",
"alert.setting.target.tip": "The selected metric object",
"alert.setting.template": "Alarm Content",
"alert.setting.template.example": "Please input notice template.Eg: ${app}.${metrics}.${metric}'s value is too high",
"alert.setting.template.label": "The notification information template sent after the alarm is triggered, see the template environment variable above",
"alert.setting.template.example": "High CPU usage detected on instance localhost",
"alert.setting.template.label": "The notification information template sent after the alarm is triggered",
"alert.setting.template.metric-name": "Metric Name",
"alert.setting.template.metric-value": "Metric Value",
"alert.setting.template.metrics-name": "Metrics Name",
@@ -400,6 +400,8 @@
"common.button.cancel": "Cancel",
"common.button.collapse": "Collapse",
"common.button.confirm": "Confirm",
"common.button.copy": "Copy",
"common.button.copy.tip": "Click to copy",
"common.button.delete": "Delete",
"common.button.detect": "Detect",
"common.button.edit": "Edit",
@@ -695,6 +697,8 @@
"monitor.new.failed": "New Monitor Failed",
"monitor.new.notify.change-to-http": "HTTPS has been disabled, and the port number has been automatically changed to 80. Please take note.",
"monitor.new.notify.change-to-https": "HTTPS has been enabled, and the port number has been automatically changed to 443. Please take note.",
"monitor.new.notify.change-to-ftp": "SFTP has been disabled, and the port number has been automatically changed to 21. Please take note.",
"monitor.new.notify.change-to-sftp": "SFTP has been enabled, and the port number has been automatically changed to 22. Please take note.",
"monitor.new.success": "New Monitor Success",
"monitor.not-found": "This Monitor Not Found",
"monitor.path.tip": "Exporter Url Endpoint Path",
@@ -702,7 +706,7 @@
"monitor.privateKey.tip": "BEGIN RSA PRIVATE KEY",
"monitor.search.app": "Type Filter",
"monitor.search.placeholder": "Search Monitor",
"monitor.search.tag": "Tag Filter",
"monitor.search.label": "Label Filter",
"monitor.sitemap.tip": "Web SITEMAP EG:/sitemap.xml",
"monitor.spinning-tip.detecting": "Available Detecting",
"monitor.status": "Task Status",
@@ -714,6 +718,10 @@
"monitor.total": "Total",
"monitor.uri.tip": "Website uri path(no ip port) EG:/console",
"monitor.url.tip": "service:jmx:rmi:///jndi/rmi://host:port/jmxrmi",
"monitor.sshHost.tip": "Required When Enabling SSH Tunnel",
"monitor.sshPort.tip": "Required When Enabling SSH Tunnel",
"monitor.sshUsername.tip": "Required When Enabling SSH Tunnel",
"monitor.sshPrivateKey.tip": "BEGIN RSA PRIVATE KEY",
"placeholder.key": "Key",
"placeholder.value": "Value",
"plugin.delete": "Delete Plugin",
@@ -755,6 +763,7 @@
"settings.system-config.locale.en_US": "English(en_US)",
"settings.system-config.locale.zh_CN": "Simplified Chinese(zh_CN)",
"settings.system-config.locale.zh_TW": "Traditional Chinese(zh_TW)",
"settings.system-config.locale.ja-JP": "Japanese(ja_JP)",
"settings.system-config.ok": "Confirm Update",
"settings.system-config.theme": "System Theme",
"settings.system-config.theme.compact": "Compact Theme",
+862
View File
@@ -0,0 +1,862 @@
{
"about.discuss": "ディスカッション",
"about.doc": "ドキュメント",
"about.gitee": "Gitee",
"about.github": "Github",
"about.help": "HertzBeatの強力なカスタマイズ、多種類のサポート、高性能および容易な拡張性は、ユーザーが迅速に独自の監視システムを構築するのを支援することを目的としています。",
"about.issue": "フィードバック",
"about.not-show-next-login": "次回ログイン時にこのポップアップを表示しない",
"about.point.1": "モニター・アラーム・通知を一体化し、Web、データベース、OS、ミドルウェア、ネットワークなどをサポート。",
"about.point.2": "使いやすさを重視し、マウスのクリックだけで完全なWebベースの操作が可能。",
"about.point.3": "強力な監視テンプレート機能で、任意のメトリックをカスタム監視。",
"about.point.4": "高性能で、コレクタークラスタ、マルチアイソレートネットワーク、クラウドエッジをサポート。",
"about.point.5": "柔軟なアラーム閾値ルールで、discord、slack、telegramなどを通じてタイムリーに通知。",
"about.point.6": "強力なステータスページを簡単に構築し、リアルタイムのステータスをユーザーと共有。",
"about.pr": "貢献",
"about.star": "スター",
"about.title": "オープンソースのリアルタイム監視システム、エージェントレス、クラスタ対応、Prometheus互換、カスタムおよびステータスページ付き。",
"about.upgrade": "アップグレード",
"alert": "アラート",
"alert.center.clear": "すべてクリア",
"alert.center.confirm.clear-all": "すべてのアラートをクリアしてもよろしいですか?",
"alert.center.confirm.delete": "削除してもよろしいですか?",
"alert.center.confirm.mark-done": "処理済みにマークしてもよろしいですか?",
"alert.center.confirm.mark-done-batch": "一括で処理済みにマークしてもよろしいですか?",
"alert.center.confirm.mark-no": "保留にマークしてもよろしいですか?",
"alert.center.confirm.mark-no-batch": "一括で保留にマークしてもよろしいですか?",
"alert.center.content": "アラート内容",
"alert.center.deal": "処理済みにマーク",
"alert.center.delete": "アラートを削除",
"alert.center.end-time": "終了時間",
"alert.center.filter-priority": "アラート優先度",
"alert.center.filter-status": "アラートステータス",
"alert.center.first-time": "開始時間",
"alert.center.labels": "タグ",
"alert.center.last-time": "アクティブ時間",
"alert.center.monitor": "所属モニター",
"alert.center.no-deal": "保留にマーク",
"alert.center.notify.no-mark": "マークする項目が選択されていません!",
"alert.center.priority": "優先度",
"alert.center.search": "アラート検索",
"alert.center.status": "ステータス",
"alert.center.target": "メトリックターゲット",
"alert.center.time": "アラート時間",
"alert.center.time.tip": "このアラート期間中に{{times}}回アラートが発生しました",
"alert.export.switch-type": "エクスポートファイル形式を選択してください!",
"alert.export.use-type": "{{type}}ファイル形式で閾値ルールをエクスポート",
"alert.group-converge.delete": "収束戦略を削除",
"alert.group-converge.edit": "収束戦略を編集",
"alert.group-converge.enable": "収束を有効化",
"alert.group-converge.group-interval": "間隔時間",
"alert.group-converge.group-interval.tip": "グループ化されたアラート通知を送信する最小間隔時間。通知が頻繁になりすぎないように、デフォルトは5分です",
"alert.group-converge.group-labels": "グループラベル",
"alert.group-converge.group-labels.add": "ラベルを追加",
"alert.group-converge.group-labels.input": "カスタムラベルを入力後、Enterキーを押してください",
"alert.group-converge.group-labels.placeholder": "ラベルを入力してください",
"alert.group-converge.group-labels.tip": "アラートラベルキーに従ってアラートメッセージをグループ化し、alertname、severity、instanceなどのラベルキーをサポートします",
"alert.group-converge.group-wait": "待機時間",
"alert.group-converge.group-wait.tip": "新しいアラートの後の待機時間。この間に受信した類似アラートはグループ化されます。デフォルトは30秒です",
"alert.group-converge.match-all": "すべて一致",
"alert.group-converge.name": "戦略名",
"alert.group-converge.name.tip": "このグループ化戦略を識別するための名前。ユニークでなければなりません",
"alert.group-converge.new": "新しい収束戦略",
"alert.group-converge.repeat-interval": "繰り返し間隔",
"alert.group-converge.repeat-interval.tip": "繰り返しアラートの通知間隔。連続するアラートで繰り返し通知が送信されないようにするための最小間隔。デフォルトは4時間です",
"alert.group-converge.seconds": "秒",
"alert.help.center": "アラームセンターは、グループ化、収束、抑制、サイレンシングなどで処理されたすべてのアラームを表示するプラットフォームですが、内部システムの閾値によってトリガーされたアラームやサードパーティのアクセスアラームが含まれます。",
"alert.help.center.link": "https://hertzbeat.apache.org/docs/help/guide",
"alert.help.group": "グループ収束は、指定されたグループラベルでアラートをグループ化およびマージし、同じ繰り返しアラートを重複排除します。\"<i>グループポリシーを追加</i>\"をクリックして設定できます。<br>閾値ルールがアラームをトリガーするか、外部アラームが報告されると、グループ収束に入り、アラームをグループ化してアラームメッセージの急増によるアラームストームを防ぎます。",
"alert.help.group.link": "https://hertzbeat.apache.org",
"alert.help.inhibit": "アラート抑制は、アラート間の抑制関係を設定するために使用されます。あるアラートが発生すると、他のアラートの生成を抑制できます。例えば、サーバーがダウンした場合、そのサーバー上のすべてのアラートを抑制できます。",
"alert.help.inhibit.link": "https://hertzbeat.apache.org/docs/help/alert_inhibit",
"alert.help.integration": "異なるサードパーティプラットフォームからのアラートを統一管理し、サードパーティの監視および可観測性システムからアラートメッセージを統合および受信し、グループ化、収束、抑制、サイレンシング、通知配信などのアクションを実行します。",
"alert.help.integration.link": "https://hertzbeat.apache.org",
"alert.help.notice": "通知は、アラームメッセージの受信者および受信方法を設定するために使用されます。アラームメッセージは、指定された方法(メール、discord、webhookなど)で受信者に送信されます。<a href='https://hertzbeat.apache.org/zh-cn/docs/help/alert_webhook'>設定手順を見るにはここをクリック。</a>。<br>“<i>通知テンプレート</i>”は、メッセージ内容の構造テンプレートです。デフォルトで組み込みテンプレートが使用されますが、テンプレートをカスタマイズしてメッセージ通知の構造をカスタマイズすることもできます。<br><span class='help_module_span'>注意⚠️: “<i>受信者</i>”を設定した後、どのメッセージをどの受信者に送信するかを指定する“<i>通知ポリシー</i>”も設定する必要があります。</span><a href='https://hertzbeat.apache.org/docs/help/alert_email'>潜在的な問題を見るにはここをクリック</a>。",
"alert.help.notice.link": "https://hertzbeat.apache.org/docs/help/alert_email",
"alert.help.setting": "閾値ルールは、メトリックアラーム閾値ルールの管理に使用されます。\"<i>新しい閾値</i>\"をクリックして監視メトリックのアラーム閾値を設定します。HertzBeatは、閾値とメトリックデータに基づいてアラームをトリガーします。<br>注意⚠️: トリガーされたアラームメッセージは[アラームセンター]で確認でき、[通知]で通知方法および担当者を設定することもできます。",
"alert.help.setting.link": "https://hertzbeat.apache.org/docs/help/alert_threshold",
"alert.help.silence": "システムメンテナンス中や夜間・週末に邪魔されたくない場合に使用するアラームサイレンス管理。<br>\"<i>新しいサイレンス戦略</i>\"をクリックしてメッセージをブロックする時間期間を設定したら、その間アラートは作動しません。",
"alert.help.silence.link": "https://hertzbeat.apache.org/docs",
"alert.inhibit.delete": "抑制ルールを削除",
"alert.inhibit.edit": "抑制ルールを編集",
"alert.inhibit.enable.tip": "この抑制ルールを有効にするかどうか",
"alert.inhibit.equal_labels": "同じラベル",
"alert.inhibit.equal_labels.common": "共通ラベル",
"alert.inhibit.equal_labels.custom": "カスタムラベル",
"alert.inhibit.equal_labels.more": "{{count}} 個のラベルが追加",
"alert.inhibit.equal_labels.placeholder": "ラベル名を入力後、Enterキーを押すか、ドロップダウンから選択してください",
"alert.inhibit.equal_labels.tip": "ソースおよびターゲットのアラートは、これらのラベルキーに対して等しい値を持つ必要があります。共通キーとしてalertname、instance、severityなどがあります",
"alert.inhibit.name": "抑制ルール名",
"alert.inhibit.name.tip": "この抑制ルールを識別するための名前。ユニークでなければなりません",
"alert.inhibit.new": "新しい抑制ルール",
"alert.inhibit.source_labels": "ソースラベル",
"alert.inhibit.source_labels.tip": "これらのラベルを含むアラートが発生すると、ターゲットアラートが抑制されます",
"alert.inhibit.target_labels": "ターゲットラベル",
"alert.inhibit.target_labels.tip": "これらのラベルに一致するアラートが抑制されます",
"alert.integration.source": "統合ソース",
"alert.integration.source.alertmanager": "Alertmanager",
"alert.integration.source.prometheus": "Prometheus",
"alert.integration.source.tencent": "Tencent Cloud",
"alert.integration.source.webhook": "デフォルトWebhook",
"alert.integration.token.desc": "HertzBeat APIにアクセスするために生成したトークン。",
"alert.integration.token.new": "トークンを生成するにはクリック",
"alert.integration.token.notice": "トークンは一度だけ表示されます。トークンを安全に保管し、他人と共有しないでください。",
"alert.integration.token.title": "アクセス・トークン",
"alert.notice.receiver": "通知受信者",
"alert.notice.receiver.delete": "受信者を削除",
"alert.notice.receiver.edit": "受信者を編集",
"alert.notice.receiver.new": "新しい受信者",
"alert.notice.receiver.next": "次に[アラート通知ポリシー]を設定してください!",
"alert.notice.receiver.people": "受信者",
"alert.notice.receiver.people.name": "受信者名",
"alert.notice.receiver.people.placeholder": "受信者を選択してください",
"alert.notice.receiver.setting": "設定",
"alert.notice.receiver.type": "通知タイプ",
"alert.notice.receiver.type.placeholder": "通知タイプを選択してください",
"alert.notice.rule": "通知ポリシー",
"alert.notice.rule.all": "すべてを配信",
"alert.notice.rule.delete": "通知ポリシーを削除",
"alert.notice.rule.edit": "通知ポリシーを編集",
"alert.notice.rule.name": "ポリシー名",
"alert.notice.rule.new": "新しい通知ポリシー",
"alert.notice.rule.period": "時間期間",
"alert.notice.rule.period-chose": "日付を選択",
"alert.notice.rule.period.custom": "カスタム",
"alert.notice.rule.period.no-limit": "無制限",
"alert.notice.rule.priority": "優先度一致",
"alert.notice.rule.priority.placeholder": "優先度を選択してください",
"alert.notice.rule.tag": "タグ一致",
"alert.notice.rule.tag.placeholder": "タグを選択してください",
"alert.notice.rule.time": "通知時間",
"alert.notice.rule.time-end": "終了時間",
"alert.notice.rule.time-start": "開始時間",
"alert.notice.send-test": "アラートテストメッセージを送信",
"alert.notice.send-test.notify.failed": "アラートテストの送信に失敗しました!",
"alert.notice.send-test.notify.success": "アラートテストの送信に成功しました!",
"alert.notice.sender.mail.enable": "メール設定を有効化",
"alert.notice.sender.mail.host": "メールサーバーアドレス",
"alert.notice.sender.mail.password": "メールパスワード",
"alert.notice.sender.mail.port": "メールポート",
"alert.notice.sender.mail.ssl": "SSLを有効化",
"alert.notice.sender.mail.starttls": "STARTTLSを有効化",
"alert.notice.sender.mail.username": "メールアカウント",
"alert.notice.sender.sms.tencent.appId": "Tencent Sms AppId",
"alert.notice.sender.sms.tencent.secretId": "Tencent Sms SecretId",
"alert.notice.sender.sms.tencent.secretKey": "Tencent Sms SecretKey",
"alert.notice.sender.sms.tencent.signName": "Tencent Sms SignName",
"alert.notice.sender.sms.tencent.templateId": "Tencent Sms TemplateId",
"alert.notice.sender.sms.type": "SMSタイプ",
"alert.notice.sender.sms.type.alibaba": "Alibaba Sms",
"alert.notice.sender.sms.type.tencent": "Tencent Sms",
"alert.notice.template": "通知テンプレート",
"alert.notice.template.content": "テンプレート内容",
"alert.notice.template.delete": "テンプレートを削除",
"alert.notice.template.edit": "テンプレートを編集",
"alert.notice.template.name": "テンプレート名",
"alert.notice.template.new": "新しいテンプレート",
"alert.notice.template.placeholder": "テンプレートを選択してください",
"alert.notice.template.preset": "テンプレートタイプ",
"alert.notice.template.preset.false": "ユーザーカスタム",
"alert.notice.template.preset.true": "システムプリセット",
"alert.notice.template.show": "テンプレート内容を表示",
"alert.notice.template.type": "通知タイプ",
"alert.notice.type.WeCom-robot": "WeCom ロボット",
"alert.notice.type.WeCom-robot-key": "WeCom ロボットキー",
"alert.notice.type.WeComApp": "WeCom アプリ",
"alert.notice.type.WeComApp-agentId": "WeCom アプリID",
"alert.notice.type.WeComApp-appSecret": "WeCom アプリシークレット",
"alert.notice.type.WeComApp-corpId": "WeCom アプリコープID",
"alert.notice.type.WeComApp-partyId": "パーティID|記号で区切る)",
"alert.notice.type.WeComApp-tagId": "タグID|記号で区切る)",
"alert.notice.type.WeComApp-userId": "ユーザーID|記号で区切る)",
"alert.notice.type.access-token": "ロボットACCESS_TOKEN",
"alert.notice.type.ding": "DingDing ロボット",
"alert.notice.type.discord": "Discord ボット",
"alert.notice.type.discord-bot-token": "Discord ボットトークン",
"alert.notice.type.discord-channel-id": "Discord チャンネルID",
"alert.notice.type.email": "メール",
"alert.notice.type.fei-shu": "FeiShu ロボット",
"alert.notice.type.fei-shu-key": "FeiShu ロボットキー",
"alert.notice.type.gotify": "Gotify",
"alert.notice.type.gotify-token": "Gotify トークン",
"alert.notice.type.phone": "電話",
"alert.notice.type.serverchan": "ServerChan",
"alert.notice.type.serverchan-token": "ServerChanトークン",
"alert.notice.type.slack": "Slack WebHook",
"alert.notice.type.slack-webHook-url": "Slack WebHook URL",
"alert.notice.type.smn": "Huawei Cloud SMN",
"alert.notice.type.smn-ak": "AK",
"alert.notice.type.smn-projectId": "プロジェクトID",
"alert.notice.type.smn-region": "リージョン",
"alert.notice.type.smn-sk": "SK",
"alert.notice.type.smn-topicUrn": "TopicUrn",
"alert.notice.type.sms": "SMS",
"alert.notice.type.telegram": "Telegram",
"alert.notice.type.telegram-bot": "Telegram ボット",
"alert.notice.type.telegram-bot-token": "Telegram ボットトークン",
"alert.notice.type.telegram-bot-user-id": "Telegram ユーザーID",
"alert.notice.type.url": "URL",
"alert.notice.type.userId": "ユーザーID",
"alert.notice.type.wechat": "Open WeChat",
"alert.notice.type.wechat-id": "WeChat OPENID",
"alert.setting.bind.available": "利用可能なモニター",
"alert.setting.bind.manage": "関連付けを管理",
"alert.setting.bind.monitors": "関連付けられたモニター",
"alert.setting.bind.monitors.tip": "この閾値ルールを特定の監視タスクに適用するように設定します。デフォルトではすべてに関連付けられていません",
"alert.setting.bind.need-save": "モニターを関連付ける前にメトリックタイプを選択してください",
"alert.setting.bind.selected": "選択されたモニター",
"alert.setting.connect": "アラート閾値をモニターに関連付ける",
"alert.setting.connect.left": "関連付けなし",
"alert.setting.connect.right": "関連付け済み",
"alert.setting.default": "グローバルデフォルト",
"alert.setting.default.tip": "このアラーム閾値設定がグローバルにこのタイプの監視に適用されるかどうか",
"alert.setting.delete": "閾値ルールを削除",
"alert.setting.edit": "閾値ルールを編集",
"alert.setting.edit.periodic": "周期的な閾値を編集",
"alert.setting.edit.realtime": "リアルタイム閾値を編集",
"alert.setting.enable": "閾値を有効化",
"alert.setting.enable.tip": "このアラーム閾値設定が有効か無効か",
"alert.setting.export": "ルールをエクスポート",
"alert.setting.expr": "閾値式",
"alert.setting.expr.example": "この式に基づいて閾値をトリガーするかどうかを計算します。例",
"alert.setting.expr.label": "この式に基づいて閾値がトリガーされたかどうかを計算および判断します。式の環境変数と演算子は上に表示されています。",
"alert.setting.expr.operator.and": "そして",
"alert.setting.expr.operator.brackets": "括弧",
"alert.setting.expr.operator.equals": "等しい",
"alert.setting.expr.operator.greater": "より大きい",
"alert.setting.expr.operator.greater-equals": "以上",
"alert.setting.expr.operator.less": "より小さい",
"alert.setting.expr.operator.less-equals": "以下",
"alert.setting.expr.operator.not-equals": "等しくない",
"alert.setting.expr.operator.or": "または",
"alert.setting.expr.tip": "サポートされている閾値トリガー式の環境変数と演算子",
"alert.setting.import": "ルールをインポート",
"alert.setting.name": "ルール名",
"alert.setting.name.tip": "ルール名はユニークである必要があります",
"alert.setting.new": "新しい閾値ルール",
"alert.setting.new.periodic": "新しい周期的閾値",
"alert.setting.new.realtime": "新しいリアルタイム閾値",
"alert.setting.number": "数値",
"alert.setting.operator": "サポートされている演算子関数",
"alert.setting.period": "実行期間",
"alert.setting.period.placeholder": "実行期間を入力してください。最小60秒",
"alert.setting.period.tip": "周期的な閾値計算の時間間隔(秒単位)、最小60秒",
"alert.setting.preview.expr": "式をプレビュー",
"alert.setting.priority.tip": "閾値をトリガーするアラームレベル。低から高へ:警告、クリティカル、緊急",
"alert.setting.promql.tip": "PromQLクエリ:sum(rate(process_cpu_seconds_total[5m])) > 0.8",
"alert.setting.recover-notice": "リカバリ通知",
"alert.setting.recover-notice.tip": "この閾値ルールに基づいてアラームが解決されたときに対応するリカバリ通知を送信するかどうか",
"alert.setting.rule": "閾値ルール",
"alert.setting.rule.label": "グラフィカルにアラーム閾値ルールを設定。複数のルール && をサポート",
"alert.setting.rule.metric.place-holder": "メトリックを選択してください",
"alert.setting.rule.numeric-value.place-holder": "数値を入力してください",
"alert.setting.rule.operator": "演算子",
"alert.setting.rule.operator.exists": "値が存在する",
"alert.setting.rule.operator.no-exists": "値が存在しない",
"alert.setting.rule.operator.str-contains": "含む",
"alert.setting.rule.operator.str-equals": "等しい",
"alert.setting.rule.operator.str-matches": "一致する",
"alert.setting.rule.operator.str-no-contains": "含まない",
"alert.setting.rule.operator.str-no-equals": "等しくない",
"alert.setting.rule.operator.str-no-matches": "一致しない",
"alert.setting.rule.string-value.place-holder": "文字列を入力してください",
"alert.setting.rule.switch-expr.0": "テンプレート閾値",
"alert.setting.rule.switch-expr.1": "コーディング閾値",
"alert.setting.search": "閾値を検索",
"alert.setting.string": "文字列",
"alert.setting.target": "メトリックタイプ",
"alert.setting.target.other": "行の他のメトリックオブジェクト",
"alert.setting.target.place-holder": "メトリックターゲットを検索または選択してください",
"alert.setting.target.system_value_row_count": "値の行数",
"alert.setting.target.tip": "選択されたメトリックオブジェクト",
"alert.setting.template": "アラーム内容",
"alert.setting.template.example": "通知テンプレートを入力してください。例:${app}.${metrics}.${metric}の値が高すぎます",
"alert.setting.template.label": "アラームがトリガーされた後に送信される通知情報のテンプレート。上記のテンプレート環境変数を参照してください。",
"alert.setting.template.metric-name": "メトリック名",
"alert.setting.template.metric-value": "メトリック値",
"alert.setting.template.metrics-name": "メトリック名",
"alert.setting.template.monitor-type": "モニタータイプ名",
"alert.setting.template.other-value": "他のメトリック値",
"alert.setting.template.tip": "サポートされている通知テンプレート環境変数",
"alert.setting.template.vars.app": "アプリケーション名",
"alert.setting.template.vars.instance": "インスタンスID",
"alert.setting.template.vars.metrics": "メトリック名",
"alert.setting.template.vars.threshold": "閾値式",
"alert.setting.template.vars.time": "トリガー時間",
"alert.setting.template.vars.tip": "メトリックや演算子を挿入",
"alert.setting.template.vars.value": "トリガー値",
"alert.setting.time": "時間",
"alert.setting.times": "トリガー回数",
"alert.setting.times.tip": "アラームを送信する前に閾値が何回トリガーされるかを設定",
"alert.setting.trigger": "アラームをトリガー",
"alert.setting.type": "閾値タイプ",
"alert.setting.type.periodic": "周期的",
"alert.setting.type.periodic.desc": "PromQLクエリを定期的に実行して閾値アラートをトリガーします。",
"alert.setting.type.realtime": "リアルタイム",
"alert.setting.type.realtime.desc": "リアルタイムメトリック計算と閾値超過時の即時アラート。",
"alert.severity": "アラームの重大度",
"alert.severity.0": "緊急",
"alert.severity.1": "クリティカル",
"alert.severity.2": "警告",
"alert.severity.all": "すべての重大度",
"alert.silence.delete": "サイレンス戦略を削除",
"alert.silence.edit": "サイレンス戦略を編集",
"alert.silence.enable": "サイレンスを有効化",
"alert.silence.labels": "タグ一致",
"alert.silence.match-all": "すべて一致",
"alert.silence.name": "サイレンス戦略名",
"alert.silence.new": "新しいサイレンス戦略",
"alert.silence.priority": "優先度一致",
"alert.silence.time": "サイレンス期間",
"alert.silence.times": "サイレンスされたアラート数",
"alert.silence.type": "サイレンスタイプ",
"alert.silence.type.cyc": "周期的サイレンス",
"alert.silence.type.once": "一時的サイレンス",
"alert.status": "アラートステータス",
"alert.status.all": "すべてのステータス",
"alert.status.firing": "アラート中",
"alert.status.resolved": "解決済み",
"annotation": "注釈",
"annotation.bind": "注釈をバインド",
"annotation.bind.tip": "注釈を使用してエンティティ情報にタグを付けることができます。たとえば、リソースに重要なイベントの注釈をバインドすることができます。",
"app.lock": "ロック解除",
"app.lock.placeholder": "解除するには何かを入力してください",
"app.login.explore.cloud": "クラウドを探索",
"app.login.explore.cloud.detail": "HertzBeatクラウドを探索するにはクリック",
"app.login.login": "ログイン",
"app.login.message-invalid-credentials": "無効なユーザー名またはパスワード",
"app.login.message-need-credential": "パスワードを入力してください",
"app.login.message-need-identifier": "ユーザー名を入力してください",
"app.login.need-change-password": "初期デフォルトパスワードを適時に更新してください!",
"app.login.notify": "ログインしてください!",
"app.login.remember-me": "ログイン状態を保持",
"app.login.tab-login-credentials": "HertzBeatにサインイン",
"app.passport.desc": "オープンソースのリアルタイム監視システム",
"app.passport.intro-1": "オープンソース、分散型",
"app.passport.intro-2": "リアルタイム監視",
"app.role.admin": "管理者",
"app.theme.compact": "コンパクトテーマ",
"app.theme.dark": "ダークテーマ",
"app.theme.default": "ライトテーマ",
"bulletin.batch.delete": "速報を一括削除",
"bulletin.delete": "速報を削除",
"bulletin.edit": "速報を編集",
"bulletin.help.content": "カスタム監視速報(ベータ)、特定のモニターの選択されたメトリックをテーブル形式で表示",
"bulletin.help.link": "https://hertzbeat.apache.org/docs/help/bulletin",
"bulletin.monitor.metrics": "モニターメトリック",
"bulletin.monitor.name": "モニタータスク名",
"bulletin.monitor.type": "モニタータイプ",
"bulletin.name": "速報名",
"bulletin.name.placeholder": "カスタム速報名を入力してください",
"bulletin.new": "新しい速報",
"collector": "コレクター",
"collector.confirm.offline": "このコレクターをオフラインにしてもよろしいですか?",
"collector.confirm.offline-batch": "コレクターを一括でオフラインにしてもよろしいですか?",
"collector.confirm.online": "このコレクターをオンラインにしてもよろしいですか?",
"collector.confirm.online-batch": "コレクターを一括でオンラインにしてもよろしいですか?",
"collector.delete": "コレクターを削除",
"collector.deploy": "コレクターをデプロイ",
"collector.deploy.close": "閉じる - トークンを保存しました",
"collector.deploy.docker": "Docker経由でデプロイ",
"collector.deploy.docker.help": "# Docker環境で以下のコマンドを実行",
"collector.deploy.docker.help.1": "# docker run -d : Dockerを通じてバックグラウンドでコンテナを起動",
"collector.deploy.docker.help.2": "# -e IDENTITY=xxx : コレクターのアイデンティティトークンを設定",
"collector.deploy.docker.help.3": "# -e MANAGER_HOST=127.0.0.1 : 接続先のメインHertzBeatサービスアドレスを設定",
"collector.deploy.docker.help.4": "# -e MODE=public : 実行モード(publicまたはprivate)を設定。パブリッククラスタまたはプライベートクラウドエッジ。",
"collector.deploy.docker.help.5": "# --name hertzbeat-collector: コンテナ名をhertzbeat-collectorに設定",
"collector.deploy.docker.help.6": "# apache/hertzbeat-collector: 公式アプリケーションイメージソース、タイムアウト時はquay.io/tancloud/hertzbeat-collectorを使用",
"collector.deploy.identity": "コレクターのトークン(IDENTITY",
"collector.deploy.identity.tip": "このトークンはユニークです。適切に保管してください。",
"collector.deploy.ok": "トークンとデプロイコマンドを生成",
"collector.deploy.package": "パッケージ経由でデプロイ",
"collector.deploy.package.gitee": "Giteeからダウンロード",
"collector.deploy.package.github": "Githubからダウンロード",
"collector.deploy.package.help": "# システムに対応するリリースパッケージhertzbeat-collector-xx.tar.gzをダウンロード",
"collector.deploy.package.help.1": "# 解凍してhertzbeat-collector/config/application.ymlファイルを設定",
"collector.deploy.package.help.2": "# ファイル内のIDENTITY、MANAGER_HOST、MODEパラメータを以下のように置き換える",
"collector.deploy.package.help.3": "# bin/startup.shまたはstartup.batWindows)を実行してコレクターを起動",
"collector.dispatched": "配信されたタスク",
"collector.help": "コレクタークラスタは、登録されたコレクタークラスタノードを管理し、現在のコレクターの状態およびタスク配信を表示し、コレクターのデプロイ、削除、オフラインなどの操作をサポートします。<br>組み込みコレクターを使用するほかに、<strong>高性能クラスタ</strong>や<strong>クラウドエッジ協調</strong>シナリオで使用するために複数のコレクターを登録できます。",
"collector.help.link": "https://hertzbeat.apache.org/docs/help/guide",
"collector.ip": "IPアドレス",
"collector.mode": "実行モード",
"collector.mode.private": "プライベートクラウドエッジ",
"collector.mode.public": "パブリッククラスタ",
"collector.name": "コレクター名",
"collector.name.placeholder": "コレクターのユニークな名前を設定してください",
"collector.node": "ノード名",
"collector.notify.no-select-offline": "オフラインにするコレクターが選択されていません!",
"collector.notify.no-select-online": "オンラインにするコレクターが選択されていません!",
"collector.offline": "コレクターをオフライン",
"collector.online": "コレクターをオンライン",
"collector.pinned": "固定されたタスク",
"collector.start-time": "開始時間",
"collector.status": "オンラインステータス",
"collector.task": "総タスク数",
"collector.version": "バージョン",
"common.annotation": "注釈",
"common.annotation.bind": "注釈をバインド",
"common.annotation.bind.tip": "リソースに重要なイベントの注釈をバインドするなど、エンティティ情報をマークするために注釈を使用できます。",
"common.button.cancel": "キャンセル",
"common.button.collapse": "折りたたむ",
"common.button.confirm": "確認",
"common.button.copy": "コピー",
"common.button.copy.tip": "クリックしてコピー",
"common.button.delete": "削除",
"common.button.detect": "検出",
"common.button.edit": "編集",
"common.button.expand": "展開",
"common.button.export": "エクスポート",
"common.button.help": "ヘルプ",
"common.button.import": "インポート",
"common.button.new": "新規作成",
"common.button.ok": "OK",
"common.button.return": "戻る",
"common.button.setting": "設定",
"common.confirm.cancel": "モニターをキャンセルしてもよろしいですか?",
"common.confirm.cancel-batch": "モニターを一括でキャンセルしてもよろしいですか?",
"common.confirm.clear-cache": "キャッシュをクリアしてもよろしいですか?",
"common.confirm.delete": "削除してもよろしいですか?",
"common.confirm.delete-batch": "一括で削除してもよろしいですか?",
"common.confirm.enable": "モニターを有効化してもよろしいですか?",
"common.confirm.enable-batch": "モニターを一括で有効化してもよろしいですか?",
"common.copy": "クリップボードにコピー",
"common.copy.button": "コピー",
"common.disable": "無効化",
"common.edit": "操作",
"common.edit-time": "更新時間",
"common.enable": "有効化",
"common.file.select": "ファイルを選択",
"common.ignore": "無視",
"common.mute": "ミュート",
"common.name": "メトリック名",
"common.new-time": "作成時間",
"common.no": "いいえ",
"common.notice": "通知",
"common.notify.apply-fail": "適用に失敗しました!",
"common.notify.apply-success": "適用に成功しました!",
"common.notify.cancel-fail": "キャンセルに失敗しました!",
"common.notify.cancel-success": "キャンセルに成功しました!",
"common.notify.clear-fail": "クリアに失敗しました!",
"common.notify.clear-success": "クリアに成功しました!",
"common.notify.copy-fail": "コピーに失敗しました!",
"common.notify.copy-success": "コピーに成功しました!",
"common.notify.delete-fail": "削除に失敗しました!",
"common.notify.delete-success": "削除に成功しました!",
"common.notify.edit-fail": "編集に失敗しました!",
"common.notify.edit-success": "編集に成功しました!",
"common.notify.enable-fail": "有効化に失敗しました!",
"common.notify.enable-success": "有効化に成功しました!",
"common.notify.export-fail": "エクスポートに失敗しました!",
"common.notify.export-success": "エクスポートに成功しました!",
"common.notify.import-fail": "インポートに失敗しました!",
"common.notify.import-success": "インポートに成功しました!",
"common.notify.mark-fail": "マークに失敗しました!",
"common.notify.mark-success": "マークに成功しました!",
"common.notify.new-fail": "追加に失敗しました!",
"common.notify.new-success": "追加に成功しました!",
"common.notify.no-select-cancel": "キャンセルする項目が選択されていません!",
"common.notify.no-select-delete": "削除する項目が選択されていません!",
"common.notify.no-select-edit": "編集する項目が選択されていません!",
"common.notify.no-select-enable": "有効化する項目が選択されていません!",
"common.notify.no-select-export": "エクスポートする項目が選択されていません!",
"common.notify.one-select-edit": "編集できるのは一つの選択だけです!",
"common.notify.operate-fail": "操作に失敗しました!",
"common.notify.operate-success": "操作に成功しました!",
"common.notify.query-fail": "モニターのクエリに失敗しました!",
"common.refresh": "更新",
"common.search": "検索",
"common.time.unit.second": "秒",
"common.total": "合計",
"common.value": "メトリック値",
"common.week.1": "月曜日",
"common.week.2": "火曜日",
"common.week.3": "水曜日",
"common.week.4": "木曜日",
"common.week.5": "金曜日",
"common.week.6": "土曜日",
"common.week.7": "日曜日",
"common.yes": "はい",
"dashboard.alerts.deal": "アラーム処理中",
"dashboard.alerts.deal-percent": "処理率",
"dashboard.alerts.distribute": "アラームの分布",
"dashboard.alerts.enter": "アラームセンターへ",
"dashboard.alerts.no": "保留中のアラームはありません",
"dashboard.alerts.num": "アラーム数",
"dashboard.alerts.title": "最近のアラーム一覧",
"dashboard.alerts.title-no": "最近の保留中のアラーム",
"dashboard.monitors.distribute": "モニターの分布",
"dashboard.monitors.formatter": " モニター ",
"dashboard.monitors.sub-title": "モニターの分布",
"dashboard.monitors.title": "監視概要",
"dashboard.monitors.total": "モニター総数",
"define.delete": "{{app}}を削除",
"define.delete.confirm": "{{app}}監視タイプを削除してもよろしいですか?削除後、このタイプの監視を追加できなくなります。",
"define.disable": "{{app}}を無効化",
"define.enable": "{{app}}を有効化",
"define.help": "モニターテンプレートは、各監視タイプ、パラメータ変数、メトリック情報、収集プロトコルなどを定義します。ドロップダウンメニューから既存の監視テンプレートを選択し、必要に応じて変更できます。左下エリアは比較エリア、右下エリアは編集エリアです。<br>\"新しいモニタータイプ\"をクリックして新しいタイプをカスタム定義することもできます。現在サポートされているプロトコルには、<a href='https://hertzbeat.apache.org/zh-cn/docs/advanced/extend-http'>HTTP</a>、<a href='https://hertzbeat.apache.org/zh-cn/docs/advanced/extend-jdbc'>JDBC</a>、<a href='https://hertzbeat.apache.org/zh-cn/docs/advanced/extend-ssh'>SSH</a>、<a href='https://hertzbeat.apache.org/zh-cn/docs/advanced/extend-jmx'>JMX</a>、<a href='https://hertzbeat.apache.org/zh-cn/docs/advanced/extend-snmp'>SNMP</a>などがあります。<a class='help_module_content' href='https://hertzbeat.apache.org/zh-cn/docs/template'>モニターテンプレート</a>。",
"define.help.link": "https://hertzbeat.apache.org/zh-cn/docs/advanced/extend-point/",
"define.hide-false.confirm": "このメニューを非表示にしてもよろしいですか?",
"define.hide-false.tip": "メインメニューに既に表示されています。非表示にしますか",
"define.hide-true.confirm": "このメニューを表示してもよろしいですか?",
"define.hide-true.tip": "メインメニューに表示されていません。表示しますか",
"define.new": "新しいモニタータイプ",
"define.new.code": "# 新しい監視タイプをここにYMLコンテンツを書いて定義してください。ドキュメントを参照:https://hertzbeat.apache.org/docs/advanced/extend-point ",
"define.save-apply": "保存して適用",
"define.save-apply.confirm": "監視定義を更新して適用してもよろしいですか?これにより監視内容に影響があります。",
"define.save-apply.no-code": "監視タイプ定義の内容を空にすることはできません。",
"label": "ラベル",
"label.bind": "ラベルをバインド",
"label.bind.tip": "ラベルを使用して分類管理できます。例:本番環境とテスト環境でリソースにラベルを割り当てる。",
"label.color": "色",
"label.delete": "ラベルを削除",
"label.description": "説明",
"label.display": "表示",
"label.edit": "ラベルを編集",
"label.id": "ID",
"label.name": "ラベル名",
"label.new": "新しいラベル",
"label.search": "ラベルを検索",
"label.setting": "ラベル",
"label.value": "ラベル値",
"labels.help": "ラベルは至る所にあります。リソースグループ化、ルール下のタグマッチングなどにラベルを適用できます。[ラベル管理]は、ラベルの統一管理に使用され、追加、削除、編集などが可能です。<br>ラベルを使用して監視リソースを分類および管理できます。例えば、本番環境とテスト環境にラベルをバインドすることができます。",
"labels.help.link": "https://hertzbeat.apache.org/zh-cn/docs/",
"menu.account": "個人",
"menu.account.binding": "アカウントバインディング",
"menu.account.center": "個人センター",
"menu.account.logout": "ログアウト",
"menu.account.security": "セキュリティ設定",
"menu.account.settings": "アカウント設定",
"menu.account.trigger": "エラーをトリガー",
"menu.advanced": "高度な設定",
"menu.advanced.collector": "コレクター",
"menu.advanced.define": "テンプレート",
"menu.advanced.labels": "ラベル",
"menu.advanced.plugins": "プラグイン",
"menu.advanced.status": "ステータス",
"menu.alert": "アラート設定",
"menu.alert.center": "アラーム",
"menu.alert.converge": "収束",
"menu.alert.dispatch": "通知",
"menu.alert.group": "グループ",
"menu.alert.inhibit": "抑制",
"menu.alert.integration": "統合",
"menu.alert.setting": "閾値",
"menu.alert.silence": "サイレンス",
"menu.clear.local.storage": "ローカルストレージをクリア",
"menu.dashboard": "ダッシュボード",
"menu.extras": "その他",
"menu.extras.about": "概要",
"menu.extras.help": "ヘルプ",
"menu.extras.setting": "設定",
"menu.extras.settings": "設定",
"menu.fullscreen": "全画面表示",
"menu.fullscreen.exit": "終了",
"menu.lang": "言語",
"menu.link.guild": "ユーザーガイド",
"menu.link.question": "FAQ",
"menu.main": "メイン",
"menu.monitor": "監視",
"menu.monitor.bigdata": "ビッグデータ監視",
"menu.monitor.bulletin": "速報",
"menu.monitor.cache": "キャッシュ監視",
"menu.monitor.center": "モニター",
"menu.monitor.cn": "クラウドネイティブ",
"menu.monitor.custom": "カスタムモニター",
"menu.monitor.db": "DB監視",
"menu.monitor.llm": "AI LLM",
"menu.monitor.mid": "ミドルウェア監視",
"menu.monitor.network": "ネットワーク監視",
"menu.monitor.os": "OS監視",
"menu.monitor.program": "プログラム監視",
"menu.monitor.prometheus": "Prometheusタスク",
"menu.monitor.promql": "データクエリ",
"menu.monitor.server": "サーバー監視",
"menu.monitor.service": "サービス監視",
"menu.monitor.webserver": "ウェブサーバー監視",
"menu.more": "もっと見る",
"menu.search.placeholder": "モニタータスク名、ホストなどを検索",
"monitor": "モニター",
"monitor.advanced": "高度な設定",
"monitor.advanced.tip": "高度な設定パラメータ",
"monitor.app": "モニタータイプ",
"monitor.app.api": "HTTP API",
"monitor.app.fullsite": "サイトマップ監視",
"monitor.app.http": "HTTP API",
"monitor.app.mysql": "MySQL",
"monitor.app.oracle": "Oracle",
"monitor.app.ping": "PING接続",
"monitor.app.port": "ポート利用可能",
"monitor.app.redis": "Redis",
"monitor.app.website": "ウェブサイト監視",
"monitor.availability": "モニターの可用性",
"monitor.cancel": "モニターを一時停止",
"monitor.category": "カテゴリ",
"monitor.category.auto": "自動タスク",
"monitor.category.bigdata": "ビッグデータ",
"monitor.category.cache": "キャッシュ",
"monitor.category.cn": "クラウドネイティブ",
"monitor.category.custom": "カスタム",
"monitor.category.db": "データベース",
"monitor.category.mid": "ミドルウェア",
"monitor.category.network": "ネットワーク",
"monitor.category.os": "OS",
"monitor.category.program": "アプリケーション",
"monitor.category.server": "サーバー監視",
"monitor.category.service": "サービス",
"monitor.category.webserver": "ウェブサーバー",
"monitor.center.help": "監視センターは監視リソース管理ポータルです。リスト形式で現在追加されたモニターを表示し、ラベルグループ化、クエリフィルタリング、および監視詳細の表示にアクセスできます。<br>モニターの追加、変更、削除、一時停止、インポート/エクスポート、一括管理などの操作を行うことができます。",
"monitor.center.help.link": "https://hertzbeat.apache.org/docs/",
"monitor.center.search.placeholder": "追加する監視タイプを検索:Linux、Redis",
"monitor.coilRegisterAddresses.tip": "RegisterAddressを入力",
"monitor.collect.time": "収集時間",
"monitor.collect.time.tip": "最後の収集時間",
"monitor.collector": "コレクター",
"monitor.collector.status.offline": "オフライン",
"monitor.collector.status.online": "オンライン",
"monitor.collector.system.default": "デフォルトシステム配信",
"monitor.collector.tip": "この監視を配信するコレクターを選択",
"monitor.content.tip": "3025020101040",
"monitor.contentType.tip": "リクエストボディタイプ",
"monitor.copy": "モニターをコピー",
"monitor.copy-monitor": "モニターをコピー",
"monitor.copy.failed": "モニターのコピーに失敗しました",
"monitor.copy.notify.one-select": "コピーできるのは一つのモニターだけです",
"monitor.copy.success": "モニターのコピーに成功しました",
"monitor.delete": "削除",
"monitor.delete-monitor": "モニターを削除",
"monitor.description": "説明",
"monitor.description.tip": "説明および備考",
"monitor.detail": "モニター詳細",
"monitor.detail.auto-refresh": "{{time}}秒後に自動更新",
"monitor.detail.basic": "監視基本情報",
"monitor.detail.chart.back": "ズームを元に戻す",
"monitor.detail.chart.no-data": "メトリックデータがありません",
"monitor.detail.chart.query-1d": "1日分をクエリ",
"monitor.detail.chart.query-1h": "1時間分をクエリ",
"monitor.detail.chart.query-1m": "1ヶ月分をクエリ",
"monitor.detail.chart.query-1w": "1週間分をクエリ",
"monitor.detail.chart.query-3m": "3ヶ月分をクエリ",
"monitor.detail.chart.query-6h": "6時間分をクエリ",
"monitor.detail.chart.save": "画像として保存",
"monitor.detail.chart.unit": "単位",
"monitor.detail.chart.zoom": "ズームイン",
"monitor.detail.close-refresh": "自動更新を閉じる",
"monitor.detail.config-refresh": "{{time}}秒ごとに自動更新を設定",
"monitor.detail.description": "説明",
"monitor.detail.history": "モニターの履歴チャート詳細",
"monitor.detail.name": "名前",
"monitor.detail.port": "ポート",
"monitor.detail.realtime": "モニターのリアルタイム詳細",
"monitor.detail.show-basic": "モニター基本情報を表示",
"monitor.detail.status": "ステータス",
"monitor.detail.time-series.unavailable": "履歴チャートを提供できません。時系列データベースを設定してください",
"monitor.detail.value.null": "値なし",
"monitor.detect": "検出",
"monitor.detect.failed": "検出に失敗しました",
"monitor.detect.success": "検出に成功しました",
"monitor.detect.tip": "モニターの利用可能ステータスを確認および検出",
"monitor.edit": "編集",
"monitor.edit-monitor": "モニターを編集",
"monitor.edit.failed": "モニターの更新に失敗しました",
"monitor.edit.success": "モニターの更新に成功しました",
"monitor.enable": "モニターを再開",
"monitor.export": "モニターをエクスポート",
"monitor.export.switch-type": "エクスポートファイル形式を選択してください!",
"monitor.export.use-type": "{{type}}ファイル形式でモニターをエクスポート",
"monitor.grafana.enabled.label": "Grafanaを有効化",
"monitor.grafana.enabled.tip": "有効化すると、監視データがGrafanaに表示されます",
"monitor.grafana.upload.label": "Grafanaテンプレートをアップロード",
"monitor.grafana.upload.tip": "Grafanaテンプレートファイルをアップロード。拡張子は.jsonをサポート",
"monitor.headerName.tip": "ヘッダ名",
"monitor.headerValue.tip": "ヘッダ値",
"monitor.holdingRegisterAddresses.tip": "RegisterAddressを入力",
"monitor.host": "ターゲットホスト",
"monitor.host.tip": "監視対象のピアIPまたはドメイン名",
"monitor.icon.bigdata": "dot-chart",
"monitor.icon.cache": "group",
"monitor.icon.center": "laptop",
"monitor.icon.cn": "cloud-server",
"monitor.icon.custom": "project",
"monitor.icon.db": "console-sql",
"monitor.icon.mid": "cluster",
"monitor.icon.network": "global",
"monitor.icon.os": "windows",
"monitor.icon.program": "code",
"monitor.icon.service": "appstore",
"monitor.icon.webserver": "database",
"monitor.import": "モニターをインポート",
"monitor.intervals": "間隔",
"monitor.intervals.tip": "周期的なデータ収集の間隔時間(秒単位)",
"monitor.keyword.tip": "監視が必要な出現キーワードを入力",
"monitor.list": "モニター一覧",
"monitor.name": "タスク名",
"monitor.name.tip": "監視タスク名",
"monitor.new": "新規作成",
"monitor.new-monitor": "新しいモニター",
"monitor.new.failed": "新しいモニターの作成に失敗しました",
"monitor.new.notify.change-to-http": "HTTPSが無効になり、ポート番号が自動的に80に変更されました。ご注意ください。",
"monitor.new.notify.change-to-https": "HTTPSが有効になり、ポート番号が自動的に443に変更されました。ご注意ください。",
"monitor.new.notify.change-to-ftp": "SFTPが無効になり、ポート番号が自動的に21に変更されましたのでご注意ください。",
"monitor.new.notify.change-to-sftp": "SFTPが有効になり、ポート番号が自動的に22に変更されましたのでご注意ください。",
"monitor.new.success": "新しいモニターの作成に成功しました",
"monitor.not-found": "このモニターは見つかりません",
"monitor.path.tip": "エクスポータのURLエンドポイントパス",
"monitor.payload.tip": "POSTまたはPUT時に利用可能",
"monitor.privateKey.tip": "BEGIN RSA PRIVATE KEY",
"monitor.search.app": "タイプフィルター",
"monitor.search.placeholder": "モニターを検索",
"monitor.search.label": "ラベルフィルター",
"monitor.sitemap.tip": "ウェブサイトのSITEMAP 例:/sitemap.xml",
"monitor.spinning-tip.detecting": "検出中",
"monitor.status": "タスクステータス",
"monitor.status.all": "すべてのステータス",
"monitor.status.down": "ダウン",
"monitor.status.paused": "一時停止",
"monitor.status.unreachable": "到達不能",
"monitor.status.up": "アップ",
"monitor.total": "合計",
"monitor.uri.tip": "ウェブサイトのURIパス(IPポートなし)例:/console",
"monitor.url.tip": "service:jmx:rmi:///jndi/rmi://host:port/jmxrmi",
"monitor.sshHost.tip": "SSHトンネルオープン時に必要",
"monitor.sshPort.tip": "SSHトンネルオープン時に必要",
"monitor.sshUsername.tip": "SSHトンネルオープン時に必要",
"monitor.sshPrivateKey.tip": "RSA秘密鍵の起動",
"placeholder.key": "キー",
"placeholder.value": "値",
"plugin.delete": "プラグインを削除",
"plugin.edit": "プラグインを編集",
"plugin.help": "プラグイン管理は、プラグインの統一管理に使用され、アップロードおよび有効化/無効化操作を含みます。プラグインメカニズムを使用して、通知以外のアラーム後のその他の操作を実行できます。<br>例えば、アラームが発生した後に特定のスクリプトやSQLを実行するためにプラグインメカニズムを使用できます。",
"plugin.help.link": "https://hertzbeat.apache.org/docs/help/plugin",
"plugin.jar.file": "Jarファイル",
"plugin.name": "プラグイン名",
"plugin.param.edit": "パラメータを編集",
"plugin.search": "プラグインを検索",
"plugin.status": "有効ステータス",
"plugin.type": "プラグインタイプ",
"plugin.type.POST_ALERT": "POST ALERT",
"plugin.type.POST_COLLECT": "POST COLLECT",
"plugin.upload": "プラグインをアップロード",
"question.link": "https://hertzbeat.apache.org/docs/help/issue/",
"settings.object-store": "ファイルサーバー設定",
"settings.object-store.obs.accessKey": "AccessKey",
"settings.object-store.obs.accessKey.placeholder": "HUAWEI CLOUDのAccess Key ID",
"settings.object-store.obs.bucketName": "バケット",
"settings.object-store.obs.bucketName.placeholder": "HUAWEI CLOUD OBSで作成したバケットの名前",
"settings.object-store.obs.endpoint": "エンドポイント",
"settings.object-store.obs.endpoint.placeholder": "HUAWEI CLOUD OBSのドメイン名、バケット名を除く",
"settings.object-store.obs.savePath": "保存パス",
"settings.object-store.obs.savePath.placeholder": "バックアップファイルを保存するパス。デフォルト値はhertzbeatです。",
"settings.object-store.obs.secretKey": "SecretKey",
"settings.object-store.obs.secretKey.placeholder": "HUAWEI CLOUDのAccess Key Secret",
"settings.object-store.type": "ファイルサーバープロバイダー",
"settings.object-store.type.database": "ローカルデータベース",
"settings.object-store.type.file": "ローカルファイル(デフォルト)",
"settings.object-store.type.obs": "HUAWEI CLOUD OBS",
"settings.server": "メッセージサーバー設定",
"settings.server.email": "メールサーバー",
"settings.server.email.setting": "メールサーバーを設定",
"settings.server.sms": "SMSサーバー",
"settings.server.sms.setting": "SMSサーバーを設定",
"settings.system-config": "システム設定",
"settings.system-config.locale": "システム言語",
"settings.system-config.locale.en_US": "英語(en_US)",
"settings.system-config.locale.zh_CN": "簡体字中国語(zh_CN)",
"settings.system-config.locale.zh_TW": "繁体字中国語(zh_TW)",
"settings.system-config.locale.ja-JP": "日本語(ja_JP)",
"settings.system-config.ok": "更新を確認",
"settings.system-config.theme": "システムテーマ",
"settings.system-config.theme.compact": "コンパクトテーマ",
"settings.system-config.theme.dark": "ダークテーマ",
"settings.system-config.theme.default": "デフォルトテーマ",
"settings.system-config.timezone": "システムタイムゾーン",
"status.component": "コンポーネント",
"status.component.config-state": "設定状態",
"status.component.config-state.tip": "ステータス計算モードが手動の場合、設定されたコンポーネントサービスのステータス。",
"status.component.delete": "コンポーネントを削除",
"status.component.desc": "コンポーネントの説明",
"status.component.desc.tip": "コンポーネントサービスの説明情報を設定",
"status.component.edit": "コンポーネントを編集",
"status.component.method": "ステータス計算モード",
"status.component.method.0": "自動計算",
"status.component.method.1": "手動設定",
"status.component.method.tip": "コンポーネントサービスのステータスを計算する方法は、ラベル関連付け監視に基づいて可用性を自動的に計算するか、ステータスを手動で設定するかです。",
"status.component.name": "サービスコンポーネント",
"status.component.name.tip": "コンポーネントサービスの表示名を設定。例:Gateway",
"status.component.new": "新しいコンポーネント",
"status.component.notify.need-org": "組織情報を最初に設定してください!",
"status.component.state": "コンポーネント状態",
"status.component.state.0": "正常",
"status.component.state.1": "異常",
"status.component.state.2": "不明",
"status.component.tag": "タグマッチ",
"status.component.tag.tip": "ステータス計算はラベルに関連付けられ、ラベルに関連付けられたすべての監視可用性ステータスをデータとして使用して、このコンポーネントのサービスステータスを計算します。",
"status.help": "HertzBeatに基づいて強力なステータスページを迅速に構築し、サービスのリアルタイムステータスをユーザーと簡単に共有できます。例えば、Githubが提供するステータスページ<a href='https://www.githubstatus.com'> https://www.githubstatus.com</a>。<br>ステータスページコンポーネントステータスと監視ステータスの連携および同期、障害イベントのメンテナンスおよび管理メカニズムなどをサポートし、透明性、専門性、およびユーザーの信頼を向上させ、コミュニケーションコストを削減します。",
"status.help.link": "https://hertzbeat.apache.org/docs/help/status",
"status.incident": "インシデント",
"status.incident.component": "影響を受けるコンポーネント",
"status.incident.delete": "インシデントを削除",
"status.incident.history": "インシデント履歴",
"status.incident.message": "メッセージを公開",
"status.incident.message-latest": "最新メッセージ",
"status.incident.message.tip.0": "このインシデントを調査中です。後ほど更新を確認してください。",
"status.incident.message.tip.1": "障害の原因を確認し、処理中です。",
"status.incident.message.tip.2": "このイベントを監視しており、処理効果を観察しています",
"status.incident.message.tip.3": "このインシデントは解決されました。ご理解とサポートに感謝します。",
"status.incident.name": "インシデント名",
"status.incident.name.tip": "現在のインシデントのタイトルを簡潔に説明してください",
"status.incident.new": "インシデントを公開",
"status.incident.public.process-time": "処理時間",
"status.incident.public.start-at": "開始時刻",
"status.incident.public.update-at": "更新時刻",
"status.incident.state": "インシデントステータス",
"status.incident.state.0": "調査中",
"status.incident.state.1": "特定済み",
"status.incident.state.2": "監視中",
"status.incident.state.3": "解決済み",
"status.incident.update": "インシデントを更新",
"status.org.color": "テーマカラー",
"status.org.color.tip": "ステータスページのテーマカラーを設定してください",
"status.org.desc": "紹介",
"status.org.desc.tip": "ステータスページに表示される組織情報の紹介",
"status.org.feedback": "フィードバックリンク",
"status.org.feedback.tip": "ユーザーのフィードバック連絡先を設定してください",
"status.org.home": "ウェブサイトリンク",
"status.org.home.tip": "組織のウェブサイトのホームページアドレスを設定してください",
"status.org.logo": "ロゴ画像",
"status.org.logo.tip": "組織のロゴ画像のURLアドレスを設定してください。svgを推奨します",
"status.org.name": "組織名",
"status.org.name.tip": "ステータスページに表示される組織チームの名前。例:TanCloud",
"status.org.state": "組織ステータス",
"status.page": "ステータスページ",
"status.public.30-day": "30日前",
"status.public.feedback": "フィードバック問題",
"status.public.org.state.0": "すべてのシステムが正常に稼働",
"status.public.org.state.1": "一部のシステムが異常",
"status.public.org.state.2": "すべてのシステムが異常",
"status.public.power-by": "Powered by Apache HertzBeat. スターを!",
"status.public.to-component": "ステータスページ",
"status.public.to-incident": "インシデント履歴",
"status.public.today": "今日",
"tag": "タグ",
"validation.confirm-password.required": "パスワードを確認してください!",
"validation.date.required": "開始日と終了日を選択してください",
"validation.email.invalid": "無効なメールアドレス!",
"validation.email.required": "メールアドレスを入力してください!",
"validation.email.wrong-format": "メールアドレスの形式が正しくありません!",
"validation.goal.required": "目標の説明を入力してください",
"validation.password.required": "パスワードを入力してください!",
"validation.password.strength.medium": "強度:中",
"validation.password.strength.msg": "少なくとも6文字を入力し、推測されやすいパスワードは使用しないでください。",
"validation.password.strength.short": "強度:短すぎます",
"validation.password.strength.strong": "強度:強",
"validation.password.twice": "入力したパスワードが一致しません!",
"validation.phone-number.required": "電話番号を入力してください!",
"validation.phone-number.wrong-format": "電話番号の形式が不正です!",
"validation.phone.invalid": "無効な電話番号!",
"validation.required": "必須項目を入力してください!",
"validation.standard.required": "メトリックを入力してください",
"validation.title.required": "タイトルを入力してください",
"validation.verification-code.invalid": "無効な認証コード。6桁である必要があります!",
"validation.verification-code.required": "認証コードを入力してください!"
}

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