mirror of
https://github.com/dromara/hertzbeat.git
synced 2026-09-17 18:19:02 +00:00
Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7f688ef9c | ||
|
|
4b5a98ea26 | ||
|
|
95852ca2bc | ||
|
|
0da2835b24 | ||
|
|
73ce238cad | ||
|
|
2763c264ad | ||
|
|
148cca0e6e | ||
|
|
acda0a9097 | ||
|
|
1c64e8022f | ||
|
|
e5d2aedfe0 | ||
|
|
e70633c256 | ||
|
|
5e69374456 | ||
|
|
6acf76c910 | ||
|
|
6d8e6f949a | ||
|
|
e6f91cae42 | ||
|
|
73e7d0ade1 | ||
|
|
79673834f1 | ||
|
|
e3a458786c |
@@ -2361,42 +2361,6 @@
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "PengJingzhao",
|
||||
"name": "彭镜肇",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/97368949?v=4",
|
||||
"profile": "https://github.com/PengJingzhao",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "gagaradio",
|
||||
"name": "Walter Jia",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/18532370?v=4",
|
||||
"profile": "https://github.com/gagaradio",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "boyucjz",
|
||||
"name": "boyucjz",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/18730041?v=4",
|
||||
"profile": "https://github.com/boyucjz",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Cyanty",
|
||||
"name": "Cyanty",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/153884653?v=4",
|
||||
"profile": "https://github.com/Cyanty",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
||||
@@ -516,10 +516,6 @@ Thanks to these wonderful people, welcome to join us:
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sarthakeash"><img src="https://avatars.githubusercontent.com/u/74091160?v=4?s=100" width="100px;" alt="Sarthak Arora"/><br /><sub><b>Sarthak Arora</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=sarthakeash" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/PengJingzhao"><img src="https://avatars.githubusercontent.com/u/97368949?v=4?s=100" width="100px;" alt="彭镜肇"/><br /><sub><b>彭镜肇</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=PengJingzhao" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/gagaradio"><img src="https://avatars.githubusercontent.com/u/18532370?v=4?s=100" width="100px;" alt="Walter Jia"/><br /><sub><b>Walter Jia</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=gagaradio" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/boyucjz"><img src="https://avatars.githubusercontent.com/u/18730041?v=4?s=100" width="100px;" alt="boyucjz"/><br /><sub><b>boyucjz</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=boyucjz" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Cyanty"><img src="https://avatars.githubusercontent.com/u/153884653?v=4?s=100" width="100px;" alt="Cyanty"/><br /><sub><b>Cyanty</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Cyanty" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -515,10 +515,6 @@ Thanks these wonderful people, welcome to join us:
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sarthakeash"><img src="https://avatars.githubusercontent.com/u/74091160?v=4?s=100" width="100px;" alt="Sarthak Arora"/><br /><sub><b>Sarthak Arora</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=sarthakeash" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/PengJingzhao"><img src="https://avatars.githubusercontent.com/u/97368949?v=4?s=100" width="100px;" alt="彭镜肇"/><br /><sub><b>彭镜肇</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=PengJingzhao" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/gagaradio"><img src="https://avatars.githubusercontent.com/u/18532370?v=4?s=100" width="100px;" alt="Walter Jia"/><br /><sub><b>Walter Jia</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=gagaradio" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/boyucjz"><img src="https://avatars.githubusercontent.com/u/18730041?v=4?s=100" width="100px;" alt="boyucjz"/><br /><sub><b>boyucjz</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=boyucjz" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Cyanty"><img src="https://avatars.githubusercontent.com/u/153884653?v=4?s=100" width="100px;" alt="Cyanty"/><br /><sub><b>Cyanty</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Cyanty" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
+5
-30
@@ -17,14 +17,11 @@
|
||||
|
||||
package org.apache.hertzbeat.alert.calculate;
|
||||
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
@@ -65,14 +62,12 @@ public class RealTimeAlertCalculator {
|
||||
private static final String KEY_PRIORITY = "__priority__";
|
||||
private static final String KEY_CODE = "__code__";
|
||||
private static final String KEY_AVAILABLE = "__available__";
|
||||
private static final String KEY_LABELS = "__labels__";
|
||||
private static final String UP = "up";
|
||||
private static final String DOWN = "down";
|
||||
private static final String KEY_ROW = "__row__";
|
||||
|
||||
private static final Pattern APP_PATTERN = Pattern.compile("equals\\(__app__,\"([^\"]+)\"\\)");
|
||||
private static final Pattern AVAILABLE_PATTERN = Pattern.compile("equals\\(__available__,\"([^\"]+)\"\\)");
|
||||
private static final Pattern LABEL_PATTERN = Pattern.compile("contains\\(__labels__,\\s*\"([^\"]+)\"\\)");
|
||||
private static final Pattern INSTANCE_PATTERN = Pattern.compile("equals\\(__instance__,\"(\\d+)\"\\)");
|
||||
private static final Pattern METRICS_PATTERN = Pattern.compile("equals\\(__metrics__,\"([^\"]+)\"\\)");
|
||||
|
||||
@@ -141,8 +136,8 @@ public class RealTimeAlertCalculator {
|
||||
Map<String, String> labels = metricsData.getLabels();
|
||||
Map<String, String> annotations = metricsData.getAnnotations();
|
||||
List<AlertDefine> thresholds = this.alertDefineService.getRealTimeAlertDefines();
|
||||
// Filter thresholds by app, metrics, labels and instance
|
||||
thresholds = filterThresholdsByAppAndMetrics(thresholds, app, metrics, labels, instance, priority);
|
||||
// Filter thresholds by app, metrics and instance
|
||||
thresholds = filterThresholdsByAppAndMetrics(thresholds, app, metrics, instance, priority);
|
||||
if (thresholds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
@@ -154,7 +149,6 @@ public class RealTimeAlertCalculator {
|
||||
commonContext.put(KEY_PRIORITY, priority);
|
||||
commonContext.put(KEY_CODE, code);
|
||||
commonContext.put(KEY_METRICS, metrics);
|
||||
commonContext.put(KEY_LABELS, String.join(",", kvLabelsToKvStringSet(labels)));
|
||||
if (priority == 0) {
|
||||
commonContext.put(KEY_AVAILABLE, metricsData.getCode() == CollectRep.Code.SUCCESS ? UP : DOWN);
|
||||
}
|
||||
@@ -270,7 +264,7 @@ public class RealTimeAlertCalculator {
|
||||
* @param priority Current priority
|
||||
* @return Filtered alert definitions
|
||||
*/
|
||||
private List<AlertDefine> filterThresholdsByAppAndMetrics(List<AlertDefine> thresholds, String app, String metrics, Map<String, String> labels, String instance, int priority) {
|
||||
private List<AlertDefine> filterThresholdsByAppAndMetrics(List<AlertDefine> thresholds, String app, String metrics, String instance, int priority) {
|
||||
return thresholds.stream()
|
||||
.filter(define -> {
|
||||
if (StringUtils.isBlank(define.getExpr())) {
|
||||
@@ -300,29 +294,19 @@ public class RealTimeAlertCalculator {
|
||||
|
||||
// Extract and check instance - optional with multiple values
|
||||
Matcher instanceMatcher = INSTANCE_PATTERN.matcher(expr);
|
||||
Matcher labelMatcher = LABEL_PATTERN.matcher(expr);
|
||||
// If no instance and instance labels specified in expr, accept all instances
|
||||
if (!instanceMatcher.find() && !labelMatcher.find()) {
|
||||
// If no instance specified in expr, accept all instances
|
||||
if (!instanceMatcher.find()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Reset matcher to check all instances
|
||||
instanceMatcher.reset();
|
||||
labelMatcher.reset();
|
||||
// If instances specified, current instance must match one of them
|
||||
while (instanceMatcher.find()) {
|
||||
if (Objects.equals(instance, instanceMatcher.group(1))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// If instance labels specified, current instance must match one of them
|
||||
Set<String> labelKvStringSet = kvLabelsToKvStringSet(labels);
|
||||
while (labelMatcher.find()) {
|
||||
String label = labelMatcher.group(1);
|
||||
if (labelKvStringSet.contains(label)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
@@ -433,13 +417,4 @@ public class RealTimeAlertCalculator {
|
||||
return Arrays.hashCode(keyList.toArray(new String[0])) + "-"
|
||||
+ Arrays.hashCode(valueList.toArray(new String[0]));
|
||||
}
|
||||
|
||||
private Set<String> kvLabelsToKvStringSet(Map<String, String> labels) {
|
||||
if (labels == null || labels.isEmpty()) {
|
||||
return Collections.singleton("");
|
||||
}
|
||||
return labels.entrySet().stream()
|
||||
.map(item -> item.getKey() + ":" + item.getValue())
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
}
|
||||
|
||||
+10
-36
@@ -63,25 +63,6 @@ public class JdbcCommonCollect extends AbstractCollect {
|
||||
|
||||
private static final String[] VULNERABLE_KEYWORDS = {"allowLoadLocalInfile", "allowLoadLocalInfileInPath", "useLocalInfile"};
|
||||
|
||||
private static final String[] BLACK_LIST = {
|
||||
// dangerous SQL commands - may cause database structure damage or data leakage
|
||||
"create trigger", "create alias", "runscript from", "shutdown", "drop table",
|
||||
"drop database", "create function", "alter system", "grant all", "revoke all",
|
||||
|
||||
// file IO related - may cause server files to be read or written
|
||||
"allowloadlocalinfile", "allowloadlocalinfileinpath", "uselocalinfile",
|
||||
|
||||
// code execution related - may result in remote code execution
|
||||
"init=", "javaobjectserializer=", "runscript", "serverstatusdiffinterceptor",
|
||||
"queryinterceptors=", "statementinterceptors=", "exceptioninterceptors=",
|
||||
|
||||
// multiple statement execution - may lead to SQL injection
|
||||
"allowmultiqueries",
|
||||
|
||||
// deserialization related - may result in remote code execution
|
||||
"autodeserialize", "detectcustomcollations",
|
||||
};
|
||||
|
||||
private final GlobalConnectionCache connectionCommonCache = GlobalConnectionCache.getInstance();
|
||||
|
||||
|
||||
@@ -350,24 +331,17 @@ public class JdbcCommonCollect extends AbstractCollect {
|
||||
if (Objects.nonNull(jdbcProtocol.getUrl())
|
||||
&& !Objects.equals("", jdbcProtocol.getUrl())
|
||||
&& jdbcProtocol.getUrl().startsWith("jdbc")) {
|
||||
// limit url length
|
||||
if (jdbcProtocol.getUrl().length() > 2048) {
|
||||
throw new IllegalArgumentException("JDBC URL length exceeds maximum limit of 2048 characters");
|
||||
// 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")
|
||||
|| url.contains("uselocalinfile") || url.contains("autodeserialize") || url.contains("detectcustomcollations")
|
||||
|| url.contains("serverstatusdiffinterceptor")) {
|
||||
throw new IllegalArgumentException("Invalid JDBC URL: contains malicious characters.");
|
||||
}
|
||||
// remove special characters
|
||||
String cleanedUrl = jdbcProtocol.getUrl().replaceAll("[\\x00-\\x1F\\x7F]", "");
|
||||
String url = cleanedUrl.toLowerCase();
|
||||
// backlist check
|
||||
for (String keyword : BLACK_LIST) {
|
||||
if (url.contains(keyword)) {
|
||||
throw new IllegalArgumentException("Invalid JDBC URL: contains potentially malicious parameter: " + keyword);
|
||||
}
|
||||
}
|
||||
// url format check
|
||||
if (!url.matches("^jdbc:[a-zA-Z0-9]+://[^\\s]+$")) {
|
||||
throw new IllegalArgumentException("Invalid JDBC URL format");
|
||||
}
|
||||
return cleanedUrl;
|
||||
// when has config jdbc url, use it
|
||||
return jdbcProtocol.getUrl();
|
||||
}
|
||||
return switch (jdbcProtocol.getPlatform()) {
|
||||
case "mysql", "mariadb" -> "jdbc:mysql://" + host + ":" + port
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
# Hertzbeat-MCP
|
||||
|
||||
## Hertzbeat-Log-MCP
|
||||
|
||||
Log MCP Service Based on GreptimeDB.
|
||||
|
||||
- GreptimeDB log writing needs to be enabled.
|
||||
|
||||
## Claude Desktop Integration (stdio)
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"hertzbeat-mcp": {
|
||||
"command": "java",
|
||||
"args": [
|
||||
"-Dspring.ai.mcp.server.stdio=true",
|
||||
"-Dspring.main.web-application-type=none",
|
||||
"-Dlogging.pattern.console=",
|
||||
"-Dgreptime.url=http://${IP}:4000",
|
||||
"-jar",
|
||||
"${PATH}/hertzbeat-mcp-2.0-SNAPSHOT.jar"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,107 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.apache.hertzbeat</groupId>
|
||||
<artifactId>hertzbeat</artifactId>
|
||||
<version>2.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hertzbeat-mcp</artifactId>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<spring-ai.version>1.0.0-M6</spring-ai.version>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-dependencies</artifactId>
|
||||
<version>3.4.2</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-bom</artifactId>
|
||||
<version>${spring-ai.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-mcp-server-webflux-spring-boot-starter</artifactId>
|
||||
<version>${spring-ai.version}</version>
|
||||
</dependency>
|
||||
<!-- json path parser-->
|
||||
<dependency>
|
||||
<groupId>com.jayway.jsonpath</groupId>
|
||||
<artifactId>json-path</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-web</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>repackage</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<source>17</source>
|
||||
<target>17</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>${maven-compiler-plugin.version}</version>
|
||||
<configuration>
|
||||
<release>${java.version}</release>
|
||||
<compilerArgs>
|
||||
<compilerArg>-parameters</compilerArg>
|
||||
</compilerArgs>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -1,44 +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.mcp.server;
|
||||
|
||||
import org.apache.hertzbeat.mcp.server.service.LogService;
|
||||
import org.springframework.ai.tool.ToolCallbackProvider;
|
||||
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
/**
|
||||
* MCP Server Application
|
||||
*/
|
||||
@SpringBootApplication
|
||||
public class McpServerApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(McpServerApplication.class, args);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ToolCallbackProvider tools(
|
||||
LogService logService) {
|
||||
return MethodToolCallbackProvider.builder()
|
||||
.toolObjects(logService)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -1,196 +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.mcp.server.service;
|
||||
|
||||
import com.jayway.jsonpath.JsonPath;
|
||||
import com.jayway.jsonpath.ReadContext;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.ai.tool.annotation.Tool;
|
||||
import org.springframework.ai.tool.annotation.ToolParam;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Log query service
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class LogService {
|
||||
|
||||
private static final String TIMESTAMP_COLUMN = "timestamp";
|
||||
private static final String SEVERITY_TEXT_COLUMN = "severity_text";
|
||||
private static final String BODY_COLUMN = "body";
|
||||
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
private final RestClient restClient;
|
||||
|
||||
public LogService(@Value("${greptime.url}") String greptimeUrl) {
|
||||
this.restClient = RestClient.builder()
|
||||
.baseUrl(greptimeUrl)
|
||||
.defaultHeader("Accept", "application/json")
|
||||
.defaultHeader("Content-Type", "application/x-www-form-urlencoded")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Tool(description = "System log query tool that supports filtering by time, log level, and content")
|
||||
public String getHertzbeatLog(
|
||||
@ToolParam(description = """
|
||||
Query system logs with support for filtering by time, log level, and content.
|
||||
|
||||
Usage:
|
||||
1. Table name: hzb_log
|
||||
2. Common query examples:
|
||||
- Get latest 10 logs: SELECT * FROM hzb_log ORDER BY timestamp DESC LIMIT 10
|
||||
- Query ERROR level logs: SELECT * FROM hzb_log WHERE severity_number=17
|
||||
- Query specific time range: SELECT * FROM hzb_log WHERE timestamp > '2024-01-01 00:00:00'
|
||||
|
||||
Field descriptions:
|
||||
1. severity_number (log level):
|
||||
- 5: DEBUG
|
||||
- 9: INFO
|
||||
- 13: WARN
|
||||
- 17: ERROR
|
||||
2. timestamp: log timestamp
|
||||
3. body: log content
|
||||
""") String querySql) {
|
||||
|
||||
if (!isValidQuery(querySql)) {
|
||||
return "Invalid query statement";
|
||||
}
|
||||
|
||||
try {
|
||||
String response = executeQuery(querySql);
|
||||
return formatQueryResults(response);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to query logs", e);
|
||||
return "Failed to query logs: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isValidQuery(String sql) {
|
||||
return sql != null && sql.toLowerCase().contains("hzb_log");
|
||||
}
|
||||
|
||||
private String executeQuery(String sql) {
|
||||
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
|
||||
formData.add("sql", sql);
|
||||
log.debug("Executing SQL query: {}", sql);
|
||||
|
||||
return restClient.post()
|
||||
.uri("/v1/sql?db=public")
|
||||
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
.body(formData)
|
||||
.retrieve()
|
||||
.body(String.class);
|
||||
}
|
||||
|
||||
private String formatQueryResults(String response) {
|
||||
ReadContext ctx = JsonPath.parse(response);
|
||||
List<Map<String, Object>> columnSchemas = ctx.read("$.output[0].records.schema.column_schemas");
|
||||
List<List<Object>> rows = ctx.read("$.output[0].records.rows");
|
||||
int totalRows = ctx.read("$.output[0].records.total_rows");
|
||||
|
||||
ColumnIndices indices = findColumnIndices(columnSchemas);
|
||||
StringBuilder result = new StringBuilder()
|
||||
.append("Query Results:\n\n")
|
||||
.append("Log Time\t\t\tLog Level\tLog Content\n")
|
||||
.append("----------------------------------------------------\n");
|
||||
|
||||
if (rows != null && !rows.isEmpty()) {
|
||||
formatRows(rows, indices, result);
|
||||
result.append("\nTotal ").append(totalRows).append(" records");
|
||||
} else {
|
||||
result.append("No data");
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
private record ColumnIndices(int timestamp, int severityText, int body) {}
|
||||
|
||||
private ColumnIndices findColumnIndices(List<Map<String, Object>> columnSchemas) {
|
||||
int timestampIndex = -1;
|
||||
int severityTextIndex = -1;
|
||||
int bodyIndex = -1;
|
||||
|
||||
for (int i = 0; i < columnSchemas.size(); i++) {
|
||||
String columnName = (String) columnSchemas.get(i).get("name");
|
||||
switch (columnName) {
|
||||
case TIMESTAMP_COLUMN -> timestampIndex = i;
|
||||
case SEVERITY_TEXT_COLUMN -> severityTextIndex = i;
|
||||
case BODY_COLUMN -> bodyIndex = i;
|
||||
default -> {
|
||||
// Ignore other columns
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new ColumnIndices(timestampIndex, severityTextIndex, bodyIndex);
|
||||
}
|
||||
|
||||
private void formatRows(List<List<Object>> rows, ColumnIndices indices, StringBuilder result) {
|
||||
for (List<Object> row : rows) {
|
||||
appendTimestamp(row, indices.timestamp(), result);
|
||||
appendSeverity(row, indices.severityText(), result);
|
||||
appendBody(row, indices.body(), result);
|
||||
result.append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
private void appendTimestamp(List<Object> row, int index, StringBuilder result) {
|
||||
if (index >= 0 && index < row.size()) {
|
||||
Object value = row.get(index);
|
||||
if (value instanceof Number) {
|
||||
long timestamp = ((Number) value).longValue();
|
||||
LocalDateTime dateTime = LocalDateTime.ofInstant(
|
||||
Instant.ofEpochMilli(timestamp / 1_000_000),
|
||||
ZoneId.systemDefault());
|
||||
result.append(DATE_FORMATTER.format(dateTime)).append("\t");
|
||||
return;
|
||||
}
|
||||
}
|
||||
result.append("Unknown time\t");
|
||||
}
|
||||
|
||||
private void appendSeverity(List<Object> row, int index, StringBuilder result) {
|
||||
if (index >= 0 && index < row.size()) {
|
||||
result.append(row.get(index)).append("\t");
|
||||
} else {
|
||||
result.append("Unknown\t");
|
||||
}
|
||||
}
|
||||
|
||||
private void appendBody(List<Object> row, int index, StringBuilder result) {
|
||||
if (index >= 0 && index < row.size()) {
|
||||
result.append(row.get(index));
|
||||
} else {
|
||||
result.append("No content");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +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.
|
||||
|
||||
spring:
|
||||
main:
|
||||
banner-mode: off
|
||||
ai:
|
||||
mcp:
|
||||
server:
|
||||
name: hertzbeat-log-analysis-server
|
||||
version: 0.1
|
||||
|
||||
#
|
||||
#logging:
|
||||
# file:
|
||||
# name:
|
||||
+2
-2
@@ -98,9 +98,9 @@ public abstract class PromqlQueryExecutor implements QueryExecutor {
|
||||
}
|
||||
HttpEntity<Void> httpEntity = new HttpEntity<>(headers);
|
||||
|
||||
UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromHttpUrl(httpPromqlProperties.url + QUERY_PATH);
|
||||
UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromHttpUrl(httpPromqlProperties.url);
|
||||
uriComponentsBuilder.queryParam(HTTP_QUERY_PARAM, queryString);
|
||||
URI uri = uriComponentsBuilder.build().toUri();
|
||||
URI uri = uriComponentsBuilder.build(true).toUri();
|
||||
ResponseEntity<PromQlQueryContent> responseEntity = restTemplate.exchange(uri,
|
||||
HttpMethod.GET, httpEntity, PromQlQueryContent.class);
|
||||
if (responseEntity.getStatusCode().is2xxSuccessful()) {
|
||||
|
||||
@@ -454,10 +454,6 @@ export default function () {
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sarthakeash"><img src="https://avatars.githubusercontent.com/u/74091160?v=4?s=100" width="100px;" alt="Sarthak Arora"/><br /><sub><b>Sarthak Arora</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=sarthakeash" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/PengJingzhao"><img src="https://avatars.githubusercontent.com/u/97368949?v=4?s=100" width="100px;" alt="彭镜肇"/><br /><sub><b>彭镜肇</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=PengJingzhao" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/gagaradio"><img src="https://avatars.githubusercontent.com/u/18532370?v=4?s=100" width="100px;" alt="Walter Jia"/><br /><sub><b>Walter Jia</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=gagaradio" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/boyucjz"><img src="https://avatars.githubusercontent.com/u/18730041?v=4?s=100" width="100px;" alt="boyucjz"/><br /><sub><b>boyucjz</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=boyucjz" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Cyanty"><img src="https://avatars.githubusercontent.com/u/153884653?v=4?s=100" width="100px;" alt="Cyanty"/><br /><sub><b>Cyanty</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Cyanty" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -91,7 +91,6 @@
|
||||
<module>hertzbeat-log</module>
|
||||
<module>hertzbeat-e2e</module>
|
||||
<module>hertzbeat-base</module>
|
||||
<module>hertzbeat-mcp</module>
|
||||
</modules>
|
||||
|
||||
<properties>
|
||||
|
||||
@@ -586,111 +586,40 @@
|
||||
nzWidth="60%"
|
||||
[nzOkLoading]="isConnectModalOkLoading"
|
||||
>
|
||||
<div *nzModalContent>
|
||||
<nz-card [nzTitle]="'alert.setting.connect.instance' | i18n">
|
||||
<nz-transfer
|
||||
[nzDataSource]="filteredTransferData"
|
||||
nzShowSelectAll="false"
|
||||
[nzRenderList]="[renderList, renderList]"
|
||||
(nzChange)="change($event)"
|
||||
style="overflow-x: scroll"
|
||||
>
|
||||
<ng-template
|
||||
#renderList
|
||||
let-items
|
||||
let-direction="direction"
|
||||
let-stat="stat"
|
||||
let-onItemSelectAll="onItemSelectAll"
|
||||
let-onItemSelect="onItemSelect"
|
||||
>
|
||||
<div>
|
||||
<div class="filter-container">
|
||||
<input
|
||||
type="text"
|
||||
nz-input
|
||||
[placeholder]="'alert.setting.filter.name' | i18n"
|
||||
[ngModel]="getSearchValue(direction)"
|
||||
(ngModelChange)="onSearchValueChange($event, direction)"
|
||||
style="width: 94%"
|
||||
/>
|
||||
</div>
|
||||
<div class="filter-container">
|
||||
<nz-select
|
||||
[ngModel]="getFilterLabels(direction)"
|
||||
(ngModelChange)="onFilterLabelsChange($event, direction)"
|
||||
name="groupLabels"
|
||||
nzMode="tags"
|
||||
[nzTokenSeparators]="[',']"
|
||||
(keydown.enter)="$event.preventDefault()"
|
||||
nzAllowClear
|
||||
[nzPlaceHolder]="'alert.setting.filter.labels' | i18n"
|
||||
style="width: 94%"
|
||||
>
|
||||
<nz-option *ngFor="let label of getMonitorLabels(direction)" [nzLabel]="label" [nzValue]="label"></nz-option>
|
||||
</nz-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nz-table #t [nzData]="$asTransferItems(items)" nzSize="small">
|
||||
<thead>
|
||||
<tr>
|
||||
<th [nzChecked]="stat.checkAll" [nzIndeterminate]="stat.checkHalf" (nzCheckedChange)="onItemSelectAll($event)"></th>
|
||||
<th *ngIf="direction == 'left'">{{ 'alert.setting.connect.left' | i18n }}</th>
|
||||
<th *ngIf="direction == 'right'">{{ 'alert.setting.connect.right' | i18n }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let data of t.data" (click)="onItemSelect(data)">
|
||||
<td [nzChecked]="!!data.checked" (nzCheckedChange)="onItemSelect(data)"></td>
|
||||
<td>{{ data.title }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</nz-table>
|
||||
</ng-template>
|
||||
</nz-transfer>
|
||||
</nz-card>
|
||||
<nz-card [nzTitle]="'alert.setting.connect.label' | i18n">
|
||||
<div class="label-input-container">
|
||||
@for (label of selectedLabels; track label) {
|
||||
<nz-tag [nzMode]="'closeable'" [nzColor]="'geekblue'" (nzOnClose)="handleDeleteLabel(label)">
|
||||
{{ label }}
|
||||
</nz-tag>
|
||||
}
|
||||
<nz-tag *ngIf="!labelInputVisible" nzNoAnimation (click)="showLabelInput()">
|
||||
<i nz-icon nzType="plus"></i>
|
||||
{{ 'alert.setting.connect.label.input' | i18n }}
|
||||
</nz-tag>
|
||||
<input
|
||||
*ngIf="labelInputVisible"
|
||||
#labelInputElement
|
||||
nz-input
|
||||
nzSize="small"
|
||||
type="text"
|
||||
[(ngModel)]="inputLabelValue"
|
||||
style="width: 78px"
|
||||
(blur)="handleLabelInputConfirm()"
|
||||
(keydown.enter)="handleLabelInputConfirm()"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<nz-empty
|
||||
*ngIf="monitorDataByLabel.length <= 0"
|
||||
nzNotFoundImage="simple"
|
||||
[nzNotFoundContent]="'alert.setting.connect.label.empty' | i18n"
|
||||
></nz-empty>
|
||||
<nz-list nzBordered *ngIf="monitorDataByLabel.length > 0">
|
||||
<div class="scroll-container">
|
||||
<nz-list-item *ngFor="let item of monitorDataByLabel">
|
||||
<span nz-typography>{{ item.title }}</span>
|
||||
<span
|
||||
><nz-tag *ngFor="let label of item.labels">{{ label }}</nz-tag></span
|
||||
>
|
||||
</nz-list-item>
|
||||
</div>
|
||||
</nz-list>
|
||||
</div>
|
||||
</nz-card>
|
||||
</div>
|
||||
<nz-transfer
|
||||
*nzModalContent
|
||||
[nzDataSource]="transferData"
|
||||
nzShowSearch="true"
|
||||
nzShowSelectAll="false"
|
||||
[nzRenderList]="[renderList, renderList]"
|
||||
(nzChange)="change($event)"
|
||||
style="overflow-x: scroll"
|
||||
>
|
||||
<ng-template
|
||||
#renderList
|
||||
let-items
|
||||
let-direction="direction"
|
||||
let-stat="stat"
|
||||
let-onItemSelectAll="onItemSelectAll"
|
||||
let-onItemSelect="onItemSelect"
|
||||
>
|
||||
<nz-table #t [nzData]="$asTransferItems(items)" nzSize="small">
|
||||
<thead>
|
||||
<tr>
|
||||
<th [nzChecked]="stat.checkAll" [nzIndeterminate]="stat.checkHalf" (nzCheckedChange)="onItemSelectAll($event)"></th>
|
||||
<th *ngIf="direction == 'left'">{{ 'alert.setting.connect.left' | i18n }}</th>
|
||||
<th *ngIf="direction == 'right'">{{ 'alert.setting.connect.right' | i18n }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let data of t.data" (click)="onItemSelect(data)">
|
||||
<td [nzChecked]="!!data.checked" (nzCheckedChange)="onItemSelect(data)"></td>
|
||||
<td>{{ data.title }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</nz-table>
|
||||
</ng-template>
|
||||
</nz-transfer>
|
||||
</nz-modal>
|
||||
|
||||
<!-- export alert definitions pop-up box -->
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
@import '@delon/theme/index';
|
||||
@import '../../../../styles/theme.less';
|
||||
::ng-deep {
|
||||
|
||||
.row {
|
||||
margin-top: 6px;
|
||||
|
||||
.ruleset {
|
||||
border: 1px solid #ccc;
|
||||
border: 1px solid #CCC;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,14 +20,14 @@
|
||||
}
|
||||
|
||||
.ruleset-invalid {
|
||||
border: none !important;
|
||||
border: none!important;
|
||||
|
||||
.ruleset {
|
||||
border: 1px solid #ff4d4f !important;
|
||||
}
|
||||
|
||||
> p {
|
||||
margin: 0 !important;
|
||||
>p {
|
||||
margin: 0!important;
|
||||
color: #ff4d4f !important;
|
||||
}
|
||||
}
|
||||
@@ -43,7 +44,7 @@
|
||||
min-width: 300px;
|
||||
overflow-x: auto;
|
||||
padding: 6px 8px;
|
||||
border: 1px solid #ccc;
|
||||
border: 1px solid #CCC;
|
||||
|
||||
.q-rule-content {
|
||||
flex: 1;
|
||||
@@ -349,19 +350,4 @@
|
||||
min-width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.label-input-container {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.scroll-container {
|
||||
height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { Component, ElementRef, HostListener, Inject, OnInit, ViewChild } from '@angular/core';
|
||||
import { Component, HostListener, Inject, OnInit, ViewChild } from '@angular/core';
|
||||
import { AbstractControl, FormBuilder, FormControl, NgForm, ValidationErrors } from '@angular/forms';
|
||||
import { I18NService } from '@core';
|
||||
import { ALAIN_I18N_TOKEN } from '@delon/theme';
|
||||
@@ -104,7 +104,6 @@ export class AlertSettingComponent implements OnInit {
|
||||
|
||||
templateEnvVars = [
|
||||
{ name: '${__instance__}', description: 'alert.setting.template.vars.instance' },
|
||||
{ name: '${__labels__}', description: 'alert.setting.template.vars.labels' },
|
||||
{ name: '${__instancename__}', description: 'alert.setting.template.vars.instance-name' },
|
||||
{ name: '${__instancehost__}', description: 'alert.setting.template.vars.instance-host' },
|
||||
{ name: '${__app__}', description: 'alert.setting.template.vars.app' },
|
||||
@@ -246,7 +245,6 @@ export class AlertSettingComponent implements OnInit {
|
||||
this.severity = '';
|
||||
this.userExpr = '';
|
||||
this.selectedMonitorIds = new Set<number>();
|
||||
this.selectedLabels = new Set<string>();
|
||||
// Set default period for periodic alert
|
||||
if (type === 'periodic') {
|
||||
this.define.period = 300;
|
||||
@@ -499,7 +497,6 @@ export class AlertSettingComponent implements OnInit {
|
||||
this.cascadeValues = this.exprToCascadeValues(this.define.expr);
|
||||
this.userExpr = this.exprToUserExpr(this.define.expr);
|
||||
this.parseMonitorIdsFromExpr(this.define.expr);
|
||||
this.parseLabelFromExpr(this.define.expr);
|
||||
this.cascadeOnChange(this.cascadeValues);
|
||||
// Wait for cascade values to be set
|
||||
setTimeout(() => {
|
||||
@@ -946,21 +943,8 @@ export class AlertSettingComponent implements OnInit {
|
||||
// start -- associate alert definition and monitoring model
|
||||
isConnectModalVisible = false;
|
||||
isConnectModalOkLoading = false;
|
||||
filteredTransferData: TransferItem[] = [];
|
||||
transferData: TransferItem[] = [];
|
||||
selectedMonitorIds = new Set<number>();
|
||||
leftMonitorLabels: Set<string> = new Set(); // All available labels on the left side
|
||||
rightMonitorLabels: Set<string> = new Set(); // All available labels on the right side
|
||||
leftSearchValue = '';
|
||||
rightSearchValue = '';
|
||||
leftFilterLabels: string[] = []; // Labels used for filtering on the left side
|
||||
rightFilterLabels: string[] = []; // Labels used for filtering on the right side
|
||||
labelInputVisible = false;
|
||||
selectedLabels: Set<string> = new Set(); // Selected labels
|
||||
inputLabelValue = '';
|
||||
labelInputElement!: ElementRef<HTMLInputElement>;
|
||||
monitorDataByLabel: any[] = [];
|
||||
|
||||
$asTransferItems = (data: unknown): TransferItem[] => data as TransferItem[];
|
||||
onConnectModalCancel() {
|
||||
this.isConnectModalVisible = false;
|
||||
@@ -995,148 +979,7 @@ export class AlertSettingComponent implements OnInit {
|
||||
}
|
||||
return e;
|
||||
});
|
||||
this.updateMonitorLabel();
|
||||
}
|
||||
|
||||
handleDeleteLabel(removedLabel: string): void {
|
||||
this.selectedLabels.delete(removedLabel);
|
||||
this.showMonitorByLabel();
|
||||
}
|
||||
|
||||
showLabelInput(): void {
|
||||
this.labelInputVisible = true;
|
||||
setTimeout(() => {
|
||||
this.labelInputElement?.nativeElement.focus();
|
||||
}, 10);
|
||||
}
|
||||
|
||||
handleLabelInputConfirm(): void {
|
||||
if (this.inputLabelValue && !this.selectedLabels.has(this.inputLabelValue)) {
|
||||
this.selectedLabels.add(this.inputLabelValue);
|
||||
}
|
||||
this.showMonitorByLabel();
|
||||
this.inputLabelValue = '';
|
||||
this.labelInputVisible = false;
|
||||
}
|
||||
|
||||
showMonitorByLabel(): void {
|
||||
const monitorsByLabel = this.transferData.filter(item => item.labels.some((label: string) => this.selectedLabels.has(label)));
|
||||
this.monitorDataByLabel = monitorsByLabel.map(item => {
|
||||
return {
|
||||
key: item.key,
|
||||
title: item.title,
|
||||
description: item.description,
|
||||
labels: item.labels
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
onFilterLabelsChange(newLabels: string[], direction: string): void {
|
||||
if (direction == 'left') {
|
||||
this.leftFilterLabels = newLabels;
|
||||
} else {
|
||||
this.rightFilterLabels = newLabels;
|
||||
}
|
||||
this.handleSearch(direction);
|
||||
}
|
||||
onSearchValueChange(value: string, direction: string): void {
|
||||
if (direction === 'left') {
|
||||
this.leftSearchValue = value;
|
||||
} else {
|
||||
this.rightSearchValue = value;
|
||||
}
|
||||
this.handleSearch(direction);
|
||||
}
|
||||
|
||||
getFilterLabels(direction: string): string[] {
|
||||
return direction === 'left' ? this.leftFilterLabels : this.rightFilterLabels;
|
||||
}
|
||||
|
||||
getSearchValue(direction: string): string {
|
||||
return direction === 'left' ? this.leftSearchValue : this.rightSearchValue;
|
||||
}
|
||||
|
||||
getMonitorLabels(direction: string): Set<string> {
|
||||
return direction === 'left' ? this.leftMonitorLabels : this.rightMonitorLabels;
|
||||
}
|
||||
|
||||
updateMonitorLabel() {
|
||||
// Extract all labels
|
||||
this.leftMonitorLabels.clear();
|
||||
this.rightMonitorLabels.clear();
|
||||
this.transferData.forEach(item => {
|
||||
item.labels.forEach((label: string) => {
|
||||
if (item.direction === 'left') {
|
||||
this.leftMonitorLabels.add(label);
|
||||
} else {
|
||||
this.rightMonitorLabels.add(label);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
handleSearch(direction: string): void {
|
||||
// keep the items that are not in the current direction
|
||||
let keepItems: TransferItem[] = this.filteredTransferData.filter(item => item.direction != direction);
|
||||
|
||||
// if the search value is empty
|
||||
if (
|
||||
(direction == 'left' && !this.leftSearchValue && !this.leftFilterLabels.length) ||
|
||||
(direction == 'right' && !this.rightSearchValue && !this.rightFilterLabels.length)
|
||||
) {
|
||||
const filteredItems = this.transferData.filter(item => item.direction === direction);
|
||||
this.filteredTransferData = [...keepItems, ...filteredItems];
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle name search
|
||||
const nameSearchResult = this.handelNameSearch(direction);
|
||||
|
||||
// Handle label search
|
||||
const labelSearchResult = this.handelLabelSearch(direction);
|
||||
|
||||
// Create Map of items by key for efficient lookup
|
||||
const nameSearchMap = new Map(nameSearchResult.map(item => [item.title, item]));
|
||||
// Find intersection - only keep items that exist in both result sets
|
||||
const filteredItems = labelSearchResult.filter(item => nameSearchMap.has(item.title));
|
||||
const result = [...keepItems, ...filteredItems];
|
||||
result.sort((a, b) => a.title.localeCompare(b.title));
|
||||
this.filteredTransferData = result;
|
||||
}
|
||||
|
||||
handelNameSearch(direction: string): TransferItem[] {
|
||||
// handel name search
|
||||
const searchValue = this.getSearchValue(direction);
|
||||
// filter the items that match the search value
|
||||
const filteredItems = this.transferData.filter(item => {
|
||||
if (item.direction !== direction) {
|
||||
// If not the current direction, skip filtering
|
||||
return false;
|
||||
}
|
||||
return item.title.toLowerCase().includes(searchValue.toLowerCase());
|
||||
});
|
||||
return filteredItems;
|
||||
}
|
||||
|
||||
handelLabelSearch(direction: string): TransferItem[] {
|
||||
// handel label search
|
||||
const filterLabels = this.getFilterLabels(direction);
|
||||
if (filterLabels.length === 0) {
|
||||
const filteredItems = this.transferData.filter(item => item.direction === direction);
|
||||
return filteredItems;
|
||||
}
|
||||
// filter the items that match the filter labels
|
||||
const filteredItems = this.transferData.filter(item => {
|
||||
if (item.direction !== direction) {
|
||||
// If not the current direction, skip filtering
|
||||
return false;
|
||||
}
|
||||
const labelSet = new Set(item.labels);
|
||||
return filterLabels.some(label => labelSet.has(label));
|
||||
});
|
||||
return filteredItems;
|
||||
}
|
||||
|
||||
// end -- associate alert definition and monitoring model
|
||||
|
||||
private cascadeValuesToExpr(values: string[]): string {
|
||||
@@ -1187,9 +1030,6 @@ export class AlertSettingComponent implements OnInit {
|
||||
// Clean up any remaining && at start/end
|
||||
.replace(/^\s*&&\s*/, '')
|
||||
.replace(/\s*&&\s*$/, '')
|
||||
// Remove monitor label binding expressions
|
||||
.replace(/&&\s*\(?(equals\(__labels__,\s*"[^"]+"\)(\s*or\s*equals\(__labels__,\s*"[^"]+"\))*)\)?/, '')
|
||||
.replace(/\(?(equals\(__labels__,\s*"[^"]+"\)(\s*or\s*equals\(__labels__,\s*"[^"]+"\))*)\)?\s*&&\s*/, '')
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1221,12 +1061,11 @@ export class AlertSettingComponent implements OnInit {
|
||||
public updateFinalExpr(): void {
|
||||
const baseExpr = this.cascadeValuesToExpr(this.cascadeValues);
|
||||
const monitorBindExpr = this.generateMonitorBindExpr();
|
||||
const monitorLabelBindExpr = this.generateMonitorLabelBindExpr();
|
||||
let thresholdExpr = '';
|
||||
if (this.cascadeValues.length >= 2 && this.cascadeValues[1] !== 'availability') {
|
||||
thresholdExpr = this.userExpr;
|
||||
}
|
||||
const exprList = [baseExpr, monitorBindExpr, monitorLabelBindExpr, thresholdExpr].filter(e => e);
|
||||
const exprList = [baseExpr, monitorBindExpr, thresholdExpr].filter(e => e);
|
||||
|
||||
this.define.expr = exprList.length > 1 ? exprList.join(' && ') : exprList[0];
|
||||
}
|
||||
@@ -1299,16 +1138,6 @@ export class AlertSettingComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
// Parse label from expression
|
||||
private parseLabelFromExpr(expr: string) {
|
||||
const labelPattern = /equals\(__labels__,\s*"([^"]+)"\)/g;
|
||||
let match;
|
||||
this.selectedLabels.clear();
|
||||
while ((match = labelPattern.exec(expr)) !== null) {
|
||||
this.selectedLabels.add(match[1]);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate monitor binding expression
|
||||
private generateMonitorBindExpr(): string {
|
||||
if (this.selectedMonitorIds.size === 0) return '';
|
||||
@@ -1318,15 +1147,6 @@ export class AlertSettingComponent implements OnInit {
|
||||
return this.selectedMonitorIds.size > 1 ? `(${idExprs})` : idExprs;
|
||||
}
|
||||
|
||||
// Generate monitor label binding expression
|
||||
private generateMonitorLabelBindExpr(): string {
|
||||
if (this.selectedLabels.size === 0) return '';
|
||||
const labelExprs = Array.from(this.selectedLabels)
|
||||
.map(label => `contains(__labels__, "${label}")`)
|
||||
.join(' or ');
|
||||
return this.selectedLabels.size > 1 ? `(${labelExprs})` : labelExprs;
|
||||
}
|
||||
|
||||
// Load monitor binds
|
||||
showConnectModal() {
|
||||
if (this.cascadeValues.length < 2) {
|
||||
@@ -1336,7 +1156,6 @@ export class AlertSettingComponent implements OnInit {
|
||||
// Parse monitor IDs from expr first
|
||||
if (this.define.expr) {
|
||||
this.parseMonitorIdsFromExpr(this.define.expr);
|
||||
this.parseLabelFromExpr(this.define.expr);
|
||||
}
|
||||
this.monitorSvc.getMonitorsByApp(this.cascadeValues[0]).subscribe(message => {
|
||||
if (message.code === 0) {
|
||||
@@ -1346,11 +1165,8 @@ export class AlertSettingComponent implements OnInit {
|
||||
key: item.id,
|
||||
title: item.name,
|
||||
description: item.host,
|
||||
direction: this.selectedMonitorIds.has(item.id) ? 'right' : 'left',
|
||||
labels: Object.entries(item.labels).map(([key, value]) => `${key}:${value}`)
|
||||
direction: this.selectedMonitorIds.has(item.id) ? 'right' : 'left'
|
||||
}));
|
||||
this.updateMonitorLabel();
|
||||
this.filteredTransferData = [...this.transferData];
|
||||
}
|
||||
});
|
||||
this.isConnectModalVisible = true;
|
||||
|
||||
@@ -26,7 +26,6 @@ import { NzCascaderModule } from 'ng-zorro-antd/cascader';
|
||||
import { NzCollapseModule } from 'ng-zorro-antd/collapse';
|
||||
import { NzDatePickerModule } from 'ng-zorro-antd/date-picker';
|
||||
import { NzDividerModule } from 'ng-zorro-antd/divider';
|
||||
import { NzEmptyModule } from 'ng-zorro-antd/empty';
|
||||
import { NzListModule } from 'ng-zorro-antd/list';
|
||||
import { NzPaginationModule } from 'ng-zorro-antd/pagination';
|
||||
import { NzRadioModule } from 'ng-zorro-antd/radio';
|
||||
@@ -77,8 +76,7 @@ const COMPONENTS: Array<Type<void>> = [
|
||||
NzBadgeModule,
|
||||
NzUploadModule,
|
||||
QueryBuilderModule,
|
||||
NzPaginationModule,
|
||||
NzEmptyModule
|
||||
NzPaginationModule
|
||||
],
|
||||
declarations: COMPONENTS
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import '~src/styles/theme';
|
||||
@import "~src/styles/theme";
|
||||
|
||||
::ng-deep {
|
||||
.monitor-select-menu-modal {
|
||||
@@ -158,16 +158,16 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
|
||||
button {
|
||||
padding: 0;
|
||||
height: auto;
|
||||
|
||||
|
||||
i {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.action-button {
|
||||
height: 32px;
|
||||
padding: 0 8px;
|
||||
@@ -197,7 +197,7 @@
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
|
||||
|
||||
i {
|
||||
margin-right: 4px;
|
||||
}
|
||||
@@ -209,7 +209,7 @@
|
||||
|
||||
.monitor-card-status {
|
||||
margin-left: auto;
|
||||
|
||||
|
||||
.ant-tag {
|
||||
margin-right: 0;
|
||||
}
|
||||
@@ -228,12 +228,12 @@
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
|
||||
.time-ago {
|
||||
margin-left: 0;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
|
||||
.label-tags {
|
||||
max-width: 100%;
|
||||
}
|
||||
@@ -245,68 +245,68 @@
|
||||
background-color: @common-background-color-dark;
|
||||
border-color: #303030;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
||||
|
||||
|
||||
&:hover {
|
||||
border-color: @primary-color;
|
||||
box-shadow: 0 1px 8px rgba(24, 144, 255, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.monitor-card-title button {
|
||||
color: @primary-color;
|
||||
}
|
||||
|
||||
|
||||
.time-ago {
|
||||
color: #a0a0a0;
|
||||
}
|
||||
|
||||
.host-item button[nzType='text'] {
|
||||
|
||||
.host-item button[nzType="text"] {
|
||||
color: #d9d9d9;
|
||||
|
||||
|
||||
&:hover {
|
||||
color: @primary-color;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.action-button {
|
||||
background-color: #303030;
|
||||
border-color: #434343;
|
||||
|
||||
|
||||
&:hover {
|
||||
background-color: #1f1f1f;
|
||||
border-color: @primary-color;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.label-tags .ant-tag {
|
||||
background-color: #303030;
|
||||
}
|
||||
|
||||
|
||||
.export-type-card {
|
||||
background-color: @common-background-color-dark;
|
||||
border-color: #303030;
|
||||
|
||||
|
||||
&:hover {
|
||||
border-color: @primary-color;
|
||||
box-shadow: 0 0 8px rgba(24, 144, 255, 0.3);
|
||||
}
|
||||
|
||||
|
||||
&.loading {
|
||||
opacity: 0.5;
|
||||
background-color: #1f1f1f;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.export-type-icon {
|
||||
color: @primary-color;
|
||||
background: rgba(24, 144, 255, 0.2);
|
||||
}
|
||||
|
||||
|
||||
.export-type-info {
|
||||
h3 {
|
||||
color: #d9d9d9;
|
||||
}
|
||||
|
||||
|
||||
p {
|
||||
color: #a0a0a0;
|
||||
}
|
||||
|
||||
@@ -229,12 +229,6 @@
|
||||
"alert.setting.connect": "Alert Threshold Associate Monitors",
|
||||
"alert.setting.connect.left": "No Associate",
|
||||
"alert.setting.connect.right": "Associated",
|
||||
"alert.setting.connect.instance": "Instance Associate",
|
||||
"alert.setting.connect.label": "Label Associate",
|
||||
"alert.setting.connect.label.input": "Input Label",
|
||||
"alert.setting.connect.label.empty": "No matching monitoring objects found, please add labels",
|
||||
"alert.setting.filter.name": "Input name to filter...",
|
||||
"alert.setting.filter.labels": "Input labels to filter...",
|
||||
"alert.setting.default": "Global Default",
|
||||
"alert.setting.default.tip": "Whether this alarm threshold configuration applies to all this type of monitoring globally",
|
||||
"alert.setting.delete": "Delete Threshold Rule",
|
||||
@@ -309,7 +303,6 @@
|
||||
"alert.setting.template.vars.instance": "Instance ID",
|
||||
"alert.setting.template.vars.instance-name": "Instance Name",
|
||||
"alert.setting.template.vars.instance-host": "Instance Host",
|
||||
"alert.setting.template.vars.labels": "Labels",
|
||||
"alert.setting.template.vars.metrics": "Metrics Name",
|
||||
"alert.setting.template.vars.threshold": "Threshold Expression",
|
||||
"alert.setting.template.vars.time": "Trigger Time",
|
||||
|
||||
@@ -229,12 +229,6 @@
|
||||
"alert.setting.connect": "アラート閾値をモニターに関連付ける",
|
||||
"alert.setting.connect.left": "関連付けなし",
|
||||
"alert.setting.connect.right": "関連付け済み",
|
||||
"alert.setting.connect.instance": "インスタンス関連",
|
||||
"alert.setting.connect.label": "ラベル関連",
|
||||
"alert.setting.connect.label.input": "ラベルを入力",
|
||||
"alert.setting.connect.label.empty": "関連するモニターが見つかりません。ラベルを追加してください",
|
||||
"alert.setting.filter.name": "名前を入力してフィルター...",
|
||||
"alert.setting.filter.labels": "ラベルを入力してフィルター...",
|
||||
"alert.setting.default": "グローバルデフォルト",
|
||||
"alert.setting.default.tip": "このアラーム閾値設定がグローバルにこのタイプの監視に適用されるかどうか",
|
||||
"alert.setting.delete": "閾値ルールを削除",
|
||||
@@ -309,7 +303,6 @@
|
||||
"alert.setting.template.vars.instance": "インスタンスID",
|
||||
"alert.setting.template.vars.instance-name": "インスタンス名",
|
||||
"alert.setting.template.vars.instance-host": "インスタンスホスト",
|
||||
"alert.setting.template.vars.labels": "タグ",
|
||||
"alert.setting.template.vars.metrics": "メトリック名",
|
||||
"alert.setting.template.vars.threshold": "閾値式",
|
||||
"alert.setting.template.vars.time": "トリガー時間",
|
||||
|
||||
+293
-771
File diff suppressed because it is too large
Load Diff
@@ -229,12 +229,6 @@
|
||||
"alert.setting.connect": "阈值规则关联监控",
|
||||
"alert.setting.connect.left": "未关联监控",
|
||||
"alert.setting.connect.right": "已关联监控",
|
||||
"alert.setting.connect.instance": "实例关联",
|
||||
"alert.setting.connect.label": "标签关联",
|
||||
"alert.setting.connect.label.input": "输入标签",
|
||||
"alert.setting.connect.label.empty": "暂无匹配的监控对象,请添加标签",
|
||||
"alert.setting.filter.name": "输入名称进行过滤...",
|
||||
"alert.setting.filter.labels": "输入标签进行过滤...",
|
||||
"alert.setting.default": "应用全局",
|
||||
"alert.setting.default.tip": "此告警阈值配置是否应用于全局所有此类型监控",
|
||||
"alert.setting.delete": "删除阈值规则",
|
||||
@@ -309,7 +303,6 @@
|
||||
"alert.setting.template.vars.instance": "实例ID",
|
||||
"alert.setting.template.vars.instance-name": "实例名称",
|
||||
"alert.setting.template.vars.instance-host": "实例Host",
|
||||
"alert.setting.template.vars.labels": "标签",
|
||||
"alert.setting.template.vars.metrics": "指标名称",
|
||||
"alert.setting.template.vars.threshold": "阈值表达式",
|
||||
"alert.setting.template.vars.time": "触发时间",
|
||||
|
||||
@@ -228,12 +228,6 @@
|
||||
"alert.setting.connect": "閾值規則關聯監控",
|
||||
"alert.setting.connect.left": "未關聯監控",
|
||||
"alert.setting.connect.right": "已關聯監控",
|
||||
"alert.setting.connect.instance": "實例關聯",
|
||||
"alert.setting.connect.label": "標籤關聯",
|
||||
"alert.setting.connect.label.input": "輸入標籤",
|
||||
"alert.setting.connect.label.empty": "暫無匹配的監控對象,請添加標籤",
|
||||
"alert.setting.filter.name": "輸入名稱進行過濾...",
|
||||
"alert.setting.filter.labels": "輸入標籤進行過濾...",
|
||||
"alert.setting.default": "應用全域",
|
||||
"alert.setting.default.tip": "此告警阈值配置是否應用于全局所有此類型監控",
|
||||
"alert.setting.delete": "刪除阈值規則",
|
||||
@@ -308,7 +302,6 @@
|
||||
"alert.setting.template.vars.instance": "实例ID",
|
||||
"alert.setting.template.vars.instance-name": "实例名称",
|
||||
"alert.setting.template.vars.instance-host": "实例Host",
|
||||
"alert.setting.template.vars.labels": "标签",
|
||||
"alert.setting.template.vars.metrics": "指標名稱",
|
||||
"alert.setting.template.vars.threshold": "閾值表達式",
|
||||
"alert.setting.template.vars.time": "觸發時間",
|
||||
|
||||
Reference in New Issue
Block a user