Compare commits

..
Author SHA1 Message Date
淞筱 0af35fb551 Merge branch 'master' into a_small_modified 2025-04-17 17:09:41 +08:00
Jast 81ad7d51b6 [Feature] Add log mcp for java (#3254) 2025-04-17 16:19:15 +08:00
a-little-fool 5813d67cd5 [improve] fix. 2025-04-16 23:03:28 +08:00
a-little-fool db471bb7ee [improve] fix. 2025-04-16 23:00:14 +08:00
a-little-fool 808a2938e7 [bug] fix format. 2025-04-16 11:49:46 +08:00
a-little-fool 97b1e906c5 [bug] fix 1.7.0 has a remote command execution vulnerability. 2025-04-16 11:36:31 +08:00
leo 9fa5b17fe4 [bugfix] fix UriComponentsBuilder in PromqlQueryExecutor (#3244) 2025-04-14 20:37:18 +08:00
tomsun28 9700161fdd [doc] add new contributors to wall (#3243)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-04-14 16:40:24 +08:00
f578df1deb [feat] enable label-based filtering and selection for monitoring thresholds (#3223)
Co-authored-by: aias00 <liuhongyu@apache.org>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-04-14 14:26:52 +08:00
eb58bb76f2 [feature] switch to prometheus online parser & add query datasource (#3215)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
Signed-off-by: leo <1552443053@qq.com>
Co-authored-by: Ceilzcx <1758619238@qq.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: aias00 <liuhongyu@apache.org>
2025-04-14 14:08:11 +08:00
23 changed files with 1656 additions and 377 deletions
+36
View File
@@ -2361,6 +2361,42 @@
"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,
+4
View File
@@ -516,6 +516,10 @@ 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>
+4
View File
@@ -515,6 +515,10 @@ 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>
@@ -17,11 +17,14 @@
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;
@@ -62,12 +65,14 @@ 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__,\"([^\"]+)\"\\)");
@@ -136,8 +141,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 and instance
thresholds = filterThresholdsByAppAndMetrics(thresholds, app, metrics, instance, priority);
// Filter thresholds by app, metrics, labels and instance
thresholds = filterThresholdsByAppAndMetrics(thresholds, app, metrics, labels, instance, priority);
if (thresholds.isEmpty()) {
return;
}
@@ -149,6 +154,7 @@ 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);
}
@@ -264,7 +270,7 @@ public class RealTimeAlertCalculator {
* @param priority Current priority
* @return Filtered alert definitions
*/
private List<AlertDefine> filterThresholdsByAppAndMetrics(List<AlertDefine> thresholds, String app, String metrics, String instance, int priority) {
private List<AlertDefine> filterThresholdsByAppAndMetrics(List<AlertDefine> thresholds, String app, String metrics, Map<String, String> labels, String instance, int priority) {
return thresholds.stream()
.filter(define -> {
if (StringUtils.isBlank(define.getExpr())) {
@@ -294,19 +300,29 @@ public class RealTimeAlertCalculator {
// Extract and check instance - optional with multiple values
Matcher instanceMatcher = INSTANCE_PATTERN.matcher(expr);
// If no instance specified in expr, accept all instances
if (!instanceMatcher.find()) {
Matcher labelMatcher = LABEL_PATTERN.matcher(expr);
// If no instance and instance labels specified in expr, accept all instances
if (!instanceMatcher.find() && !labelMatcher.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());
@@ -417,4 +433,13 @@ 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());
}
}
@@ -63,6 +63,25 @@ 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();
@@ -331,17 +350,24 @@ public class JdbcCommonCollect extends AbstractCollect {
if (Objects.nonNull(jdbcProtocol.getUrl())
&& !Objects.equals("", jdbcProtocol.getUrl())
&& jdbcProtocol.getUrl().startsWith("jdbc")) {
// 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.");
// limit url length
if (jdbcProtocol.getUrl().length() > 2048) {
throw new IllegalArgumentException("JDBC URL length exceeds maximum limit of 2048 characters");
}
// when has config jdbc url, use it
return jdbcProtocol.getUrl();
// 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;
}
return switch (jdbcProtocol.getPlatform()) {
case "mysql", "mariadb" -> "jdbc:mysql://" + host + ":" + port
+27
View File
@@ -0,0 +1,27 @@
# 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"
]
}
}
}
```
+107
View File
@@ -0,0 +1,107 @@
<?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>
@@ -0,0 +1,44 @@
/*
* 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();
}
}
@@ -0,0 +1,196 @@
/*
* 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");
}
}
}
@@ -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.
spring:
main:
banner-mode: off
ai:
mcp:
server:
name: hertzbeat-log-analysis-server
version: 0.1
#
#logging:
# file:
# name:
@@ -98,9 +98,9 @@ public abstract class PromqlQueryExecutor implements QueryExecutor {
}
HttpEntity<Void> httpEntity = new HttpEntity<>(headers);
UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromHttpUrl(httpPromqlProperties.url);
UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromHttpUrl(httpPromqlProperties.url + QUERY_PATH);
uriComponentsBuilder.queryParam(HTTP_QUERY_PARAM, queryString);
URI uri = uriComponentsBuilder.build(true).toUri();
URI uri = uriComponentsBuilder.build().toUri();
ResponseEntity<PromQlQueryContent> responseEntity = restTemplate.exchange(uri,
HttpMethod.GET, httpEntity, PromQlQueryContent.class);
if (responseEntity.getStatusCode().is2xxSuccessful()) {
+4
View File
@@ -454,6 +454,10 @@ 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>
+1
View File
@@ -91,6 +91,7 @@
<module>hertzbeat-log</module>
<module>hertzbeat-e2e</module>
<module>hertzbeat-base</module>
<module>hertzbeat-mcp</module>
</modules>
<properties>
@@ -586,40 +586,111 @@
nzWidth="60%"
[nzOkLoading]="isConnectModalOkLoading"
>
<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>
<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-modal>
<!-- export alert definitions pop-up box -->
@@ -1,12 +1,11 @@
@import '@delon/theme/index';
@import '../../../../styles/theme.less';
::ng-deep {
.row {
margin-top: 6px;
.ruleset {
border: 1px solid #CCC;
border: 1px solid #ccc;
}
}
@@ -20,14 +19,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;
}
}
@@ -44,7 +43,7 @@
min-width: 300px;
overflow-x: auto;
padding: 6px 8px;
border: 1px solid #CCC;
border: 1px solid #ccc;
.q-rule-content {
flex: 1;
@@ -350,4 +349,19 @@
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, HostListener, Inject, OnInit, ViewChild } from '@angular/core';
import { Component, ElementRef, 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,6 +104,7 @@ 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' },
@@ -245,6 +246,7 @@ 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;
@@ -497,6 +499,7 @@ 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(() => {
@@ -943,8 +946,21 @@ 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;
@@ -979,7 +995,148 @@ 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 {
@@ -1030,6 +1187,9 @@ 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*/, '')
);
}
@@ -1061,11 +1221,12 @@ 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, thresholdExpr].filter(e => e);
const exprList = [baseExpr, monitorBindExpr, monitorLabelBindExpr, thresholdExpr].filter(e => e);
this.define.expr = exprList.length > 1 ? exprList.join(' && ') : exprList[0];
}
@@ -1138,6 +1299,16 @@ 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 '';
@@ -1147,6 +1318,15 @@ 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) {
@@ -1156,6 +1336,7 @@ 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) {
@@ -1165,8 +1346,11 @@ export class AlertSettingComponent implements OnInit {
key: item.id,
title: item.name,
description: item.host,
direction: this.selectedMonitorIds.has(item.id) ? 'right' : 'left'
direction: this.selectedMonitorIds.has(item.id) ? 'right' : 'left',
labels: Object.entries(item.labels).map(([key, value]) => `${key}:${value}`)
}));
this.updateMonitorLabel();
this.filteredTransferData = [...this.transferData];
}
});
this.isConnectModalVisible = true;
+3 -1
View File
@@ -26,6 +26,7 @@ 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';
@@ -76,7 +77,8 @@ const COMPONENTS: Array<Type<void>> = [
NzBadgeModule,
NzUploadModule,
QueryBuilderModule,
NzPaginationModule
NzPaginationModule,
NzEmptyModule
],
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;
}
+7
View File
@@ -229,6 +229,12 @@
"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",
@@ -303,6 +309,7 @@
"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",
+7
View File
@@ -229,6 +229,12 @@
"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": "閾値ルールを削除",
@@ -303,6 +309,7 @@
"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": "トリガー時間",
File diff suppressed because it is too large Load Diff
+7
View File
@@ -229,6 +229,12 @@
"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": "删除阈值规则",
@@ -303,6 +309,7 @@
"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": "触发时间",
+7
View File
@@ -228,6 +228,12 @@
"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": "刪除阈值規則",
@@ -302,6 +308,7 @@
"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": "觸發時間",