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
50 changed files with 2165 additions and 1571 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
@@ -18,11 +18,14 @@
package org.apache.hertzbeat.collector.collect.http;
import static org.apache.hertzbeat.common.constants.SignConstants.RIGHT_DASH;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.StringReader;
import java.net.ConnectException;
@@ -34,9 +37,7 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.net.ssl.SSLException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
@@ -44,13 +45,14 @@ import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.collector.collect.AbstractCollect;
import org.apache.hertzbeat.collector.collect.common.http.CommonHttpClient;
import org.apache.hertzbeat.collector.collect.http.promethus.AbstractPrometheusParse;
import org.apache.hertzbeat.collector.collect.http.promethus.PrometheusParseCreator;
import org.apache.hertzbeat.collector.collect.http.promethus.exporter.ExporterParser;
import org.apache.hertzbeat.collector.collect.http.promethus.exporter.MetricFamily;
import org.apache.hertzbeat.collector.collect.prometheus.parser.MetricFamily;
import org.apache.hertzbeat.collector.collect.prometheus.parser.OnlineParser;
import org.apache.hertzbeat.collector.constants.CollectorConstants;
import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
import org.apache.hertzbeat.collector.util.CollectUtil;
@@ -103,10 +105,13 @@ import java.util.Collections;
*/
@Slf4j
public class HttpCollectImpl extends AbstractCollect {
private static final Map<Long, ExporterParser> EXPORTER_PARSER_TABLE = new ConcurrentHashMap<>();
private final Set<Integer> defaultSuccessStatusCodes = Stream.of(HttpStatus.SC_OK, HttpStatus.SC_CREATED,
HttpStatus.SC_ACCEPTED, HttpStatus.SC_MULTIPLE_CHOICES, HttpStatus.SC_MOVED_PERMANENTLY,
HttpStatus.SC_MOVED_TEMPORARILY).collect(Collectors.toSet());
private final Set<Integer> defaultSuccessStatusCodes = Set.of(
HttpStatus.SC_OK,
HttpStatus.SC_CREATED,
HttpStatus.SC_ACCEPTED,
HttpStatus.SC_MULTIPLE_CHOICES,
HttpStatus.SC_MOVED_PERMANENTLY,
HttpStatus.SC_MOVED_TEMPORARILY);
@Override
public void preCheck(Metrics metrics) throws IllegalArgumentException {
@@ -127,7 +132,7 @@ public class HttpCollectImpl extends AbstractCollect {
if (CollectionUtils.isEmpty(httpProtocol.getSuccessCodes())) {
httpProtocol.setSuccessCodes(List.of(HttpStatus.SC_OK + ""));
}
HttpContext httpContext = createHttpContext(metrics.getHttp());
HttpUriRequest request = createHttpRequest(metrics.getHttp());
try (CloseableHttpResponse response = CommonHttpClient.getHttpClient().execute(request, httpContext)) {
@@ -139,10 +144,11 @@ public class HttpCollectImpl extends AbstractCollect {
builder.setMsg(NetworkConstants.STATUS_CODE + SignConstants.BLANK + statusCode);
return;
}
// todo This code converts an InputStream directly to a String. For large data in Prometheus exporters,
// this could create large objects, potentially impacting JVM memory space significantly.
// Option 1: Parse using InputStream, but this requires significant code changes;
// Option 2: Manually trigger garbage collection, similar to how it's done in Dubbo for large inputs.
/*
this could create large objects, potentially impacting JVM memory space significantly.
Option 1: Parse using InputStream, but this requires significant code changes;
Option 2: Manually trigger garbage collection, similar to how it's done in Dubbo for large inputs.
*/
String resp = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
if (!StringUtils.hasText(resp)) {
log.info("http response entity is empty, status: {}.", statusCode);
@@ -156,7 +162,7 @@ public class HttpCollectImpl extends AbstractCollect {
case DispatchConstants.PARSE_PROM_QL ->
parseResponseByPromQl(resp, metrics.getAliasFields(), metrics.getHttp(), builder);
case DispatchConstants.PARSE_PROMETHEUS ->
parseResponseByPrometheusExporter(resp, metrics.getAliasFields(), builder);
parseResponseByPrometheusExporter(response.getEntity().getContent(), metrics.getAliasFields(), builder);
case DispatchConstants.PARSE_XML_PATH ->
parseResponseByXmlPath(resp, metrics, builder, responseTime);
case DispatchConstants.PARSE_WEBSITE ->
@@ -594,36 +600,22 @@ public class HttpCollectImpl extends AbstractCollect {
prometheusParser.handle(resp, aliasFields, http, builder);
}
private void parseResponseByPrometheusExporter(String resp, List<String> aliasFields,
CollectRep.MetricsData.Builder builder) {
if (!EXPORTER_PARSER_TABLE.containsKey(builder.getId())) {
EXPORTER_PARSER_TABLE.put(builder.getId(), new ExporterParser());
private void parseResponseByPrometheusExporter(InputStream content, List<String> aliasFields, CollectRep.MetricsData.Builder builder) throws IOException {
Map<String, MetricFamily> metricFamilyMap = OnlineParser.parseMetrics(content);
if (metricFamilyMap == null || metricFamilyMap.isEmpty()) {
return;
}
ExporterParser parser = EXPORTER_PARSER_TABLE.get(builder.getId());
Map<String, MetricFamily> metricFamilyMap = parser.textToMetric(resp);
String metrics = builder.getMetrics();
if (metricFamilyMap.containsKey(metrics)) {
MetricFamily metricFamily = metricFamilyMap.get(metrics);
for (MetricFamily.Metric metric : metricFamily.getMetricList()) {
Map<String, String> labelMap = metric.getLabelPair()
Map<String, String> labelMap = metric.getLabels()
.stream()
.collect(Collectors.toMap(MetricFamily.Label::getName, MetricFamily.Label::getValue));
CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder();
for (String aliasField : aliasFields) {
if ("value".equals(aliasField)) {
if (metric.getCounter() != null) {
valueRowBuilder.addColumn(String.valueOf(metric.getCounter().getValue()));
} else if (metric.getGauge() != null) {
valueRowBuilder.addColumn(String.valueOf(metric.getGauge().getValue()));
} else if (metric.getUntyped() != null) {
valueRowBuilder.addColumn(String.valueOf(metric.getUntyped().getValue()));
} else if (metric.getInfo() != null) {
valueRowBuilder.addColumn(String.valueOf(metric.getInfo().getValue()));
} else if (metric.getSummary() != null) {
valueRowBuilder.addColumn(String.valueOf(metric.getSummary().getValue()));
} else if (metric.getHistogram() != null) {
valueRowBuilder.addColumn(String.valueOf(metric.getHistogram().getValue()));
}
valueRowBuilder.addColumn(String.valueOf(metric.getValue()));
} else {
String columnValue = labelMap.get(aliasField);
valueRowBuilder.addColumn(columnValue == null ? CommonConstants.NULL_VALUE : columnValue);
@@ -1,432 +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.collector.collect.http.promethus.exporter;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.collector.collect.http.promethus.ParseException;
import org.apache.hertzbeat.common.util.StrBuffer;
/**
* Resolves the data passed by prometheus's exporter interface http:xxx/metrics
* Reference: prometheus text_parse.go code, entry: TextToMetricFamilies
*/
@Slf4j
public class ExporterParser {
private static final String HELP = "HELP";
private static final String TYPE = "TYPE";
private static final String EOF = "EOF";
private static final String METRIC_NAME_LABEL = ".name";
private static final String QUANTILE_LABEL = "quantile";
private static final String BUCKET_LABEL = "le";
private static final String NAME_LABEL = "__name__";
private static final String SUM_SUFFIX = "_sum";
private static final String COUNT_SUFFIX = "_count";
private static final char LEFT_CURLY_BRACKET = '{';
private static final char RIGHT_CURLY_BRACKET = '}';
private static final char EQUALS = '=';
private static final char QUOTES = '"';
private static final char ENTER = '\n';
private static final char SPACE = ' ';
private static final char COMMA = ',';
private final Lock lock = new ReentrantLock();
private MetricFamily currentMetricFamily;
private String currentQuantile;
private String currentBucket;
public Map<String, MetricFamily> textToMetric(String resp) {
// key: metric name, value: metric family
Map<String, MetricFamily> metricMap = new ConcurrentHashMap<>(10);
lock.lock();
try {
String[] lines = resp.split("\n");
for (String line : lines) {
this.parseLine(metricMap, new StrBuffer(line));
}
return metricMap;
} catch (Exception e) {
log.error("parse prometheus exporter data error, msg: {}", e.getMessage());
} finally {
lock.unlock();
}
return metricMap;
}
private void parseLine(Map<String, MetricFamily> metricMap, StrBuffer buffer) {
buffer.skipBlankTabs();
if (buffer.isEmpty()) {
return;
}
switch (buffer.charAt(0)) {
case '#' -> {
buffer.read();
this.currentMetricFamily = null;
this.parseComment(metricMap, buffer);
}
case ENTER -> {
}
default -> {
this.currentBucket = null;
this.currentQuantile = null;
this.parseMetric(buffer);
}
}
}
private void parseComment(Map<String, MetricFamily> metricMap, StrBuffer buffer) {
buffer.skipBlankTabs();
if (buffer.isEmpty()) {
return;
}
String token = this.readTokenUnitWhitespace(buffer);
if (EOF.equals(token)) {
return;
}
if (!HELP.equals(token) && !TYPE.equals(token)) {
log.error("parse comment error {}, start without {} or {}", buffer.toStr(), HELP, TYPE);
return;
}
String metricName = this.readTokenAsMetricName(buffer);
this.currentMetricFamily = metricMap.computeIfAbsent(metricName, key -> new MetricFamily());
this.currentMetricFamily.setName(metricName);
switch (token) {
case HELP -> this.parseHelp(buffer);
case TYPE -> this.parseType(buffer);
default -> {
}
}
}
private void parseHelp(StrBuffer line) {
line.skipBlankTabs();
this.currentMetricFamily.setHelp(line.toStr());
}
private void parseType(StrBuffer line) {
line.skipBlankTabs();
String type = line.toStr().toLowerCase();
MetricType metricType = MetricType.getType(type);
if (metricType == null) {
throw new ParseException("pare type error");
}
this.currentMetricFamily.setMetricType(metricType);
}
private void parseMetric(StrBuffer buffer) {
String metricName = this.readTokenAsMetricName(buffer);
MetricFamily.Label label = new MetricFamily.Label();
label.setName(METRIC_NAME_LABEL);
label.setValue(metricName);
if (metricName.isEmpty()) {
log.error("error parse metric, metric name is null, line: {}", buffer.toStr());
return;
}
List<MetricFamily.Metric> metricList = this.currentMetricFamily.getMetricList();
if (metricList == null) {
metricList = new ArrayList<>();
this.currentMetricFamily.setMetricList(metricList);
}
// TODO For the time being, the data is displayed in the form of labels. If there is a better chart display method in the future, we will optimize it.
MetricFamily.Metric metric = new MetricFamily.Metric();
metricList.add(metric);
metric.setLabelPair(new ArrayList<>());
metric.getLabelPair().add(label);
this.readLabels(metric, buffer);
}
private void readLabels(MetricFamily.Metric metric, StrBuffer buffer) {
buffer.skipBlankTabs();
if (buffer.isEmpty()) {
return;
}
if (buffer.charAt(0) == LEFT_CURLY_BRACKET) {
buffer.read();
this.startReadLabelName(metric, buffer);
} else {
this.readLabelValue(metric, null, buffer);
}
}
private void startReadLabelName(MetricFamily.Metric metric, StrBuffer buffer) {
buffer.skipBlankTabs();
if (buffer.isEmpty()) {
return;
}
if (buffer.charAt(0) == RIGHT_CURLY_BRACKET) {
buffer.read();
buffer.skipBlankTabs();
if (buffer.isEmpty()) {
return;
}
this.readLabelValue(metric, new MetricFamily.Label(), buffer);
return;
}
String labelName = this.readTokenAsLabelName(buffer);
if (labelName.isEmpty() || labelName.equals(NAME_LABEL)) {
throw new ParseException("invalid label name" + labelName + ", label name size = 0 or label name equals " + NAME_LABEL);
}
MetricFamily.Label label = new MetricFamily.Label();
label.setName(labelName);
if (buffer.read() != EQUALS) {
throw new ParseException("parse error, not match the format of labelName=labelValue");
}
this.startReadLabelValue(metric, label, buffer);
}
private void startReadLabelValue(MetricFamily.Metric metric, MetricFamily.Label label, StrBuffer buffer) {
buffer.skipBlankTabs();
if (buffer.isEmpty()) {
return;
}
char c = buffer.read();
if (c != QUOTES) {
throw new ParseException("expected '\"' at start of label value, line: " + buffer.toStr());
}
String labelValue = this.readTokenAsLabelValue(buffer);
label.setValue(labelValue);
if (!this.isValidLabelValue(labelValue)) {
throw new ParseException("no valid label value: " + labelValue);
}
if (this.currentMetricFamily.getMetricType().equals(MetricType.SUMMARY) && label.getName().equals(QUANTILE_LABEL)) {
this.currentQuantile = labelValue;
} else if (this.currentMetricFamily.getMetricType().equals(MetricType.HISTOGRAM) && label.getName().equals(BUCKET_LABEL)) {
this.currentBucket = labelValue;
}
metric.getLabelPair().add(label);
if (buffer.isEmpty()) {
return;
}
c = buffer.read();
switch (c) {
case COMMA -> this.startReadLabelName(metric, buffer);
case RIGHT_CURLY_BRACKET -> this.readLabelValue(metric, label, buffer);
default -> throw new ParseException("expected '}' or ',' at end of label value, line: " + buffer.toStr());
}
}
private void readLabelValue(MetricFamily.Metric metric, MetricFamily.Label label, StrBuffer buffer) {
buffer.skipBlankTabs();
if (buffer.isEmpty()) {
return;
}
switch (this.currentMetricFamily.getMetricType()) {
case INFO -> {
MetricFamily.Info info = new MetricFamily.Info();
info.setValue(buffer.toDouble());
metric.setInfo(info);
}
case COUNTER -> {
MetricFamily.Counter counter = new MetricFamily.Counter();
counter.setValue(buffer.toDouble());
metric.setCounter(counter);
}
case GAUGE -> {
MetricFamily.Gauge gauge = new MetricFamily.Gauge();
gauge.setValue(buffer.toDouble());
metric.setGauge(gauge);
}
case UNTYPED -> {
MetricFamily.Untyped untyped = new MetricFamily.Untyped();
untyped.setValue(buffer.toDouble());
metric.setUntyped(untyped);
}
case SUMMARY -> {
// For the time being, the data is displayed in the form of labels. If there is a better chart display method in the future, we will optimize it.
MetricFamily.Summary summary = new MetricFamily.Summary();
summary.setValue(buffer.toDouble());
metric.setSummary(summary);
}
case HISTOGRAM -> {
// For the time being, the data is displayed in the form of labels. If there is a better chart display method in the future, we will optimize it.
MetricFamily.Histogram histogram = new MetricFamily.Histogram();
histogram.setValue(buffer.toDouble());
metric.setHistogram(histogram);
}
default -> throw new ParseException("no such type in metricFamily");
}
}
/**
* Reads the token before the first whitespace
*
* @param buffer A line data object
* @return token unit
*/
private String readTokenUnitWhitespace(StrBuffer buffer) {
StringBuilder builder = new StringBuilder();
while (!buffer.isEmpty()) {
char c = buffer.read();
if (c == SPACE) {
break;
}
builder.append(c);
}
return builder.toString();
}
/**
* Gets the name of the metric
*
* @param buffer A line data object
* @return token name
*/
private String readTokenAsMetricName(StrBuffer buffer) {
buffer.skipBlankTabs();
StringBuilder builder = new StringBuilder();
if (this.isValidMetricNameStart(buffer.charAt(0))) {
while (!buffer.isEmpty()) {
char c = buffer.read();
if (!this.isValidMetricNameContinuation(c)) {
buffer.rollback();
break;
}
builder.append(c);
}
return builder.toString();
}
throw new ParseException("parse metric name error");
}
/**
* Gets the name of the label
*
* @param buffer A line data object
* @return label name
*/
private String readTokenAsLabelName(StrBuffer buffer) {
buffer.skipBlankTabs();
StringBuilder builder = new StringBuilder();
char c = buffer.read();
if (this.isValidLabelNameStart(c)) {
builder.append(c);
while (!buffer.isEmpty()) {
c = buffer.read();
if (!this.isValidLabelNameContinuation(c)) {
buffer.rollback();
break;
}
builder.append(c);
}
return builder.toString();
}
throw new ParseException("parse label name error");
}
/**
* Gets the value of the label
*
* @param buffer A line data object
* @return label value
*/
private String readTokenAsLabelValue(StrBuffer buffer) {
StringBuilder builder = new StringBuilder();
boolean escaped = false;
while (!buffer.isEmpty()) {
char c = buffer.read();
// Handle '\\' escape sequences
if (escaped) {
switch (c) {
case QUOTES, '\\' -> builder.append(c);
case 'n' -> builder.append('\n');
default -> throw new ParseException("parse label value error");
}
escaped = false;
} else {
switch (c) {
case QUOTES -> {
return builder.toString();
}
case ENTER -> throw new ParseException("parse label value error, next line");
case '\\' -> escaped = true;
default -> builder.append(c);
}
}
}
return builder.toString();
}
/**
* Checks whether a character conforms to the first character rule for metric names
*
* @param c metric character
* @return true/false
*/
private boolean isValidMetricNameStart(char c) {
return isValidLabelNameStart(c) || c == ':';
}
/**
* Checks whether a character conforms to rules for metric name characters other than the first
*
* @param c metric character
* @return true/false
*/
private boolean isValidMetricNameContinuation(char c) {
return isValidLabelNameContinuation(c) || c == ':';
}
/**
* Checks whether a character conforms to the first character rule for label names
*
* @param c metric character
* @return true/false
*/
private boolean isValidLabelNameStart(char c) {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_';
}
/**
* Checks whether a character conforms to rules for label name characters other than the first
*
* @param c metric character
* @return true/false
*/
private boolean isValidLabelNameContinuation(char c) {
return isValidLabelNameStart(c) || (c >= '0' && c <= '9');
}
/**
* Checks if a string is a valid UTF-8 encoded string
*
* @param s label value
* @return true/false
*/
private boolean isValidLabelValue(String s) {
return s != null;
}
private boolean isSum(String s) {
return s != null && s.endsWith(SUM_SUFFIX);
}
private boolean isCount(String s) {
return s != null && s.endsWith(COUNT_SUFFIX);
}
}
@@ -1,252 +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.collector.collect.http.promethus.exporter;
import java.util.ArrayList;
import java.util.List;
import lombok.Data;
import lombok.ToString;
/**
* MetricFamily.
*/
@Data
@ToString
public class MetricFamily {
/**
* metric name
*/
private String name;
/**
* metric help
*/
private String help;
/**
* metric type
*/
private MetricType metricType;
/**
* Specific metric
*/
private List<Metric> metricList;
/**
* Metric
*/
@Data
public static class Metric {
/**
* Label data, mainly corresponding to the content within {}
*/
private List<Label> labelPair;
/**
* info
*/
private Info info;
/**
* gauge
*/
private Gauge gauge;
/**
* counter
*/
private Counter counter;
/**
* summary
*/
private Summary summary;
/**
* untyped
*/
private Untyped untyped;
/**
* histogram
*/
private Histogram histogram;
/**
* timestampMs
*/
private Long timestampMs;
}
/**
* Label
*/
@Data
public static class Label {
/**
* name
*/
private String name;
/**
* value
*/
private String value;
}
/**
* Info
*/
@Data
public static class Info {
/**
* value
*/
private double value;
}
/**
* Counter
*/
@Data
public static class Counter {
/**
* value
*/
private double value;
// Exemplar
}
/**
* Gauge
*/
@Data
public static class Gauge {
/**
* value
*/
private double value;
}
/**
* untyped
*/
@Data
public static class Untyped {
/**
* value
*/
private double value;
}
/**
* Summary
*/
@Data
public static class Summary {
/**
* value
*/
private double value;
/**
* count
*/
private long count;
/**
* sum
*/
private double sum;
/**
* quantileList
*/
private List<Quantile> quantileList = new ArrayList<>();
}
/**
* Quantile
*/
@Data
public static class Quantile {
/**
* Corresponding to the quantile field in Prometheus
*/
private double xLabel;
/**
* value
*/
private double value;
}
/**
* Histogram
*/
@Data
public static class Histogram {
/**
* value
*/
private double value;
/**
* count
*/
private long count;
/**
* sum
*/
private double sum;
/**
* bucketList
*/
private List<Bucket> bucketList = new ArrayList<>();
}
/**
* Bucket
*/
@Data
public static class Bucket {
/**
* cumulativeCount
*/
private long cumulativeCount;
/**
* upperBound
*/
private double upperBound;
}
}
@@ -1,52 +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.collector.collect.http.promethus.exporter;
/**
* prometheus metrics type
*/
public enum MetricType {
// for string metric info
INFO("info"),
// Represents a monotonically increasing counter, e.g., counting occurrences
COUNTER("counter"),
// A metric type that can fluctuate up and down, e.g., CPU usage rate
GAUGE("gauge"),
SUMMARY("summary"),
UNTYPED("untyped"),
HISTOGRAM("histogram");
private final String value;
MetricType(String value) {
this.value = value;
}
public String getValue() {
return value;
}
public static MetricType getType(String value) {
for (MetricType metricType : values()) {
if (metricType.getValue().equals(value)) {
return metricType;
}
}
return null;
}
}
@@ -19,6 +19,7 @@ package org.apache.hertzbeat.collector.collect.prometheus;
import static org.apache.hertzbeat.common.constants.SignConstants.RIGHT_DASH;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.net.ConnectException;
import java.net.UnknownHostException;
@@ -33,8 +34,7 @@ import java.util.stream.Stream;
import javax.net.ssl.SSLException;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.collector.collect.common.http.CommonHttpClient;
import org.apache.hertzbeat.common.entity.dto.MetricFamily;
import org.apache.hertzbeat.collector.collect.prometheus.parser.TextParser;
import org.apache.hertzbeat.collector.collect.prometheus.parser.MetricFamily;
import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
import org.apache.hertzbeat.collector.util.CollectUtil;
import org.apache.hertzbeat.common.constants.CommonConstants;
@@ -46,6 +46,7 @@ import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.util.Base64Util;
import org.apache.hertzbeat.common.util.CommonUtil;
import org.apache.hertzbeat.common.util.IpDomainUtil;
import org.apache.hertzbeat.collector.collect.prometheus.parser.OnlineParser;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpHost;
import org.apache.http.HttpStatus;
@@ -64,7 +65,6 @@ import org.apache.http.impl.auth.DigestScheme;
import org.apache.http.impl.client.BasicAuthCache;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;
import org.springframework.http.MediaType;
import org.springframework.util.StringUtils;
@@ -99,25 +99,12 @@ public class PrometheusAutoCollectImpl {
builder.setMsg(NetworkConstants.STATUS_CODE + SignConstants.BLANK + statusCode);
return null;
}
// todo: The InputStream is directly converted to a String here
// For large data in the Prometheus exporter, this can generate large objects, which could severely impact JVM memory space
// todo: Option one: Use InputStream for parsing, but this requires significant code changes
// Option two: Manually trigger garbage collection, which can be referenced from Dubbo for long i
String resp = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
long collectTime = System.currentTimeMillis();
builder.setTime(collectTime);
if (resp == null || !StringUtils.hasText(resp)) {
log.error("http response content is empty, status: {}.", statusCode);
try {
return parseResponseByPrometheusExporter(response.getEntity().getContent(), builder);
} catch (Exception e) {
log.info("parse error: {}.", e.getMessage(), e);
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg("http response content is empty");
} else {
try {
return parseResponseByPrometheusExporter(resp, metrics.getAliasFields(), builder);
} catch (Exception e) {
log.info("parse error: {}.", e.getMessage(), e);
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg("parse response data error:" + e.getMessage());
}
builder.setMsg("parse response data error:" + e.getMessage());
}
} catch (ClientProtocolException e1) {
String errorMsg = CommonUtil.getMessageFromThrowable(e1);
@@ -168,10 +155,12 @@ public class PrometheusAutoCollectImpl {
}
}
private List<CollectRep.MetricsData> parseResponseByPrometheusExporter(String resp, List<String> aliasFields,
CollectRep.MetricsData.Builder builder) {
Map<String, MetricFamily> metricFamilyMap = TextParser.textToMetricFamilies(resp);
private List<CollectRep.MetricsData> parseResponseByPrometheusExporter(InputStream inputStream, CollectRep.MetricsData.Builder builder) throws IOException {
Map<String, MetricFamily> metricFamilyMap = OnlineParser.parseMetrics(inputStream);
List<CollectRep.MetricsData> metricsDataList = new LinkedList<>();
if (metricFamilyMap == null) {
return metricsDataList;
}
for (Map.Entry<String, MetricFamily> entry : metricFamilyMap.entrySet()) {
builder.clearFields();
builder.clearValues();
@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.apache.hertzbeat.common.entity.dto;
package org.apache.hertzbeat.collector.collect.prometheus.parser;
import java.util.List;
import lombok.Data;
@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.apache.hertzbeat.common.util;
package org.apache.hertzbeat.collector.collect.prometheus.parser;
import java.util.ArrayList;
import java.util.HashMap;
@@ -23,7 +23,6 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.common.entity.dto.MetricFamily;
import java.io.IOException;
import java.io.InputStream;
@@ -38,7 +37,7 @@ import java.util.concurrent.ConcurrentHashMap;
@Slf4j
public class OnlineParser {
private static final Map<Integer, Integer> escapeMap = new HashMap<>();
private static final Map<Integer, Integer> escapeMap = new HashMap<>(8);
static {
escapeMap.put((int) 'n', (int) '\n');
@@ -51,6 +50,30 @@ public class OnlineParser {
escapeMap.put((int) '\\', (int) '\\');
}
private OnlineParser() {
}
public static Map<String, MetricFamily> parseMetrics(InputStream inputStream) throws IOException {
Map<String, MetricFamily> metricFamilyMap = new ConcurrentHashMap<>(10);
try {
int i = getChar(inputStream);
while (i != -1) {
if (i == '#' || i == '\n') {
skipToLineEnd(inputStream).maybeEol().maybeEof().noElse();
} else {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append((char) i);
parseMetric(inputStream, metricFamilyMap, stringBuilder);
}
i = getChar(inputStream);
}
} catch (FormatException e) {
log.error("prometheus parser failed because of wrong input format. {}", e.getMessage());
return null;
}
return metricFamilyMap;
}
private static class FormatException extends Exception {
public FormatException() {
@@ -133,7 +156,7 @@ public class OnlineParser {
return this.i;
}
private int getInt() throws FormatException {
private int getInt() {
return this.i;
}
@@ -153,14 +176,9 @@ public class OnlineParser {
}
}
private static CharChecker parseOneChar(InputStream inputStream) throws IOException, FormatException {
int i = getChar(inputStream);
return new CharChecker(i);
}
private static CharChecker parseOneDouble(InputStream inputStream, StringBuilder stringBuilder) throws IOException, FormatException {
int i = getChar(inputStream);
while ((i >= '0' && i <= '9') || (i >= 'a' && i <= 'z') || (i >= 'A' && i <= 'Z') || i == '-' || i == '+' || i == 'e' || i == '.') {
while (i >= '0' && i <= '9' || i >= 'a' && i <= 'z' || i >= 'A' && i <= 'Z' || i == '-' || i == '+' || i == '.') {
stringBuilder.append((char) i);
i = getChar(inputStream);
}
@@ -199,17 +217,10 @@ public class OnlineParser {
if (i == '\\') {
i = getChar(inputStream);
switch (i) {
case 'n':
stringBuilder.append('\n');
break;
case '\\':
stringBuilder.append('\\');
break;
case '\"':
stringBuilder.append('\"');
break;
default:
throw new FormatException();
case 'n' -> stringBuilder.append('\n');
case '\\' -> stringBuilder.append('\\');
case '\"' -> stringBuilder.append('\"');
default -> throw new FormatException();
}
} else {
stringBuilder.append((char) i);
@@ -285,7 +296,7 @@ public class OnlineParser {
}
private static CharChecker parseMetric(InputStream inputStream, Map<String, MetricFamily> metricFamilyMap, StringBuilder stringBuilder) throws IOException, FormatException {
MetricFamily metricFamily = null;
MetricFamily metricFamily;
MetricFamily.Metric metric = new MetricFamily.Metric();
int i = parseMetricName(inputStream, stringBuilder).maybeSpace().maybeLeftBracket().noElse();
String metricName = stringBuilder.toString();
@@ -333,26 +344,4 @@ public class OnlineParser {
metricFamily.getMetricList().add(metric);
return new CharChecker(i);
}
public static Map<String, MetricFamily> parseMetrics(InputStream inputStream) throws IOException {
Map<String, MetricFamily> metricFamilyMap = new ConcurrentHashMap<>(10);
try {
int i = getChar(inputStream);
while (i != -1) {
if (i == '#' || i == '\n') {
skipToLineEnd(inputStream).maybeEol().maybeEof().noElse();
} else {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append((char) i);
parseMetric(inputStream, metricFamilyMap, stringBuilder);
}
i = getChar(inputStream);
}
} catch (FormatException e) {
log.error("prometheus parser failed because of wrong input format. {}", e.getMessage());
return null;
}
return metricFamilyMap;
}
}
@@ -24,14 +24,15 @@ import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.collector.collect.http.promethus.ParseException;
import org.apache.hertzbeat.common.entity.dto.MetricFamily;
import org.apache.hertzbeat.common.util.StrBuffer;
/**
* Resolves the data passed by prometheus's exporter interface http:xxx/metrics
* Reference: prometheus text_parse.go code, entry: TextToMetricFamilies
* recommend use OnlineParser
*/
@Slf4j
@Deprecated(since = "1.7.0")
public class TextParser {
private static final String NAME_LABEL = "__name__";
private static final char LEFT_CURLY_BRACKET = '{';
@@ -45,7 +46,6 @@ public class TextParser {
/**
* parser prometheus exporter text metrics data
* todo use inputStream bytebuffer instead of resp string
* @param resp txt data
* @return metrics family
*/
@@ -58,13 +58,13 @@ public class PushCollectImpl extends AbstractCollect {
private static final Map<Long, Long> timeMap = new ConcurrentHashMap<>();
// ms
private static final Integer timeout = 3000;
private static final Integer DEFAULT_TIMEOUT = 3000;
private static final Integer SUCCESS_CODE = 200;
// It's hard to determine how long ago the first data collection was, because there's no way to know when the last collection occurred.
// This makes it difficult to avoid re-collecting data after a restart. The default is 30 seconds
private static final Integer firstCollectInterval = 30000;
private static final Integer FIRST_COLLECT_INTERVAL = 30000;
@Override
public void preCheck(Metrics metrics) throws IllegalArgumentException {
@@ -80,7 +80,7 @@ public class PushCollectImpl extends AbstractCollect {
long monitorId = builder.getId();
PushProtocol pushProtocol = metrics.getPush();
Long time = timeMap.getOrDefault(monitorId, curTime - firstCollectInterval);
Long time = timeMap.getOrDefault(monitorId, curTime - FIRST_COLLECT_INTERVAL);
timeMap.put(monitorId, curTime);
HttpContext httpContext = createHttpContext(pushProtocol);
@@ -145,10 +145,10 @@ public class PushCollectImpl extends AbstractCollect {
//requestBuilder.setUri(pushProtocol.getUri());
if (timeout > 0) {
if (DEFAULT_TIMEOUT > 0) {
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(timeout)
.setSocketTimeout(timeout)
.setConnectTimeout(DEFAULT_TIMEOUT)
.setSocketTimeout(DEFAULT_TIMEOUT)
.setRedirectsEnabled(true)
.build();
requestBuilder.setConfig(requestConfig);
@@ -1,79 +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.collector.collect.http.promethus.exporter;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.Map;
import org.junit.jupiter.api.Test;
/**
* Test case for {@link ExporterParser}
*/
class ExporterParserTest {
@Test
void textToMetric() {
String resp = """
# HELP disk_total_bytes Total space for path
# TYPE disk_total_bytes gauge
disk_total_bytes{path="C:\\\\hertzbeat\\\\repo\\\\testpath",} 4.29496725504E11
# HELP go_gc_cycles_automatic_gc_cycles_total Count of completed GC cycles generated by the Go runtime.
# TYPE go_gc_cycles_automatic_gc_cycles_total counter
go_gc_cycles_automatic_gc_cycles_total 0
# HELP go_gc_cycles_forced_gc_cycles_total Count of completed GC cycles forced by the application.
# TYPE go_gc_cycles_forced_gc_cycles_total counter
go_gc_cycles_forced_gc_cycles_total 0
# HELP go_gc_cycles_total_gc_cycles_total Count of all completed GC cycles.
# TYPE go_gc_cycles_total_gc_cycles_total counter
go_gc_cycles_total_gc_cycles_total 0
# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.
# TYPE go_gc_duration_seconds summary
go_gc_duration_seconds{quantile="0"} 0
go_gc_duration_seconds{quantile="0.25"} 0
go_gc_duration_seconds{quantile="0.5"} 0
go_gc_duration_seconds{quantile="0.75"} 0
go_gc_duration_seconds{quantile="1"} 0
# TYPE jvm info
# HELP jvm VM version info
jvm_info{runtime="OpenJDK Runtime Environment",vendor="Azul Systems, Inc.",version="11.0.13+8-LTS"} 1.0
# TYPE jvm_gc_collection_seconds summary
# HELP jvm_gc_collection_seconds Time spent in a given JVM garbage collector in seconds.
jvm_gc_collection_seconds_count{gc="G1 Young Generation"} 10.0
jvm_gc_collection_seconds_sum{gc="G1 Young Generation"} 0.051
jvm_gc_collection_seconds_count{gc="G1 Old Generation"} 0.0
jvm_gc_collection_seconds_sum{gc="G1 Old Generation"} 0.0
# TYPE resource_group_aggregate_usage_secs summary
resource_group_aggregate_usage_secs{cluster="standalone",quantile="0.5"} 2.69245E-4
resource_group_aggregate_usage_secs{cluster="standalone",quantile="0.9"} 3.49601E-4
resource_group_aggregate_usage_secs_count{cluster="standalone"} 13.0
resource_group_aggregate_usage_secs_sum{cluster="standalone"} 0.004832498
resource_group_aggregate_usage_secs_created{cluster="standalone"} 1.715842140749E9
# TYPE metadata_store_ops_latency_ms histogram
metadata_store_ops_latency_ms_bucket{cluster="standalone",name="metadata-store",type="get",status="success",le="1.0"} 59.0
metadata_store_ops_latency_ms_bucket{cluster="standalone",name="metadata-store",type="get",status="success",le="3.0"} 61.0
metadata_store_ops_latency_ms_bucket{cluster="standalone",name="metadata-store",type="get",status="success",le="5.0"} 61.0
# EOF""";
ExporterParser parser = new ExporterParser();
Map<String, MetricFamily> metricFamilyMap = parser.textToMetric(resp);
assertEquals(5, metricFamilyMap.get("resource_group_aggregate_usage_secs").getMetricList().size());
assertEquals(3, metricFamilyMap.get("metadata_store_ops_latency_ms").getMetricList().size());
assertEquals(5, metricFamilyMap.get("go_gc_duration_seconds").getMetricList().size());
assertEquals(9, metricFamilyMap.size());
}
}
@@ -17,8 +17,6 @@
package org.apache.hertzbeat.collector.collect.prometheus.parser;
import org.apache.hertzbeat.common.entity.dto.MetricFamily;
import org.apache.hertzbeat.common.util.OnlineParser;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
@@ -27,8 +25,12 @@ import java.io.InputStream;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.fail;
class OnlineParserTest {
@@ -46,13 +48,13 @@ class OnlineParserTest {
String str = """
# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.
# TYPE go_gc_duration_seconds summary
go_gc_duration_seconds { quantile="0"} 2.0209e-05 321312
go_gc_duration_seconds{ quantile = "0.25" } 6.6917e-05
go_gc_duration_seconds{quantile="0"} 2.0209e-05
go_gc_duration_seconds{quantile="0.25"} 6.6917e-05
go_gc_duration_seconds{quantile="0.5"} -Inf
go_gc_duration_seconds{ quantile = "0.75"} +Inf
go_gc_duration_seconds{quantile="0.75"} +Inf
go_gc_duration_seconds{quantile="1"} NaN
go_gc_duration_seconds_sum 0.001134793 321314
go_gc_duration_seconds_count 5 43
go_gc_duration_seconds_sum 0.001134793
go_gc_duration_seconds_count 5
# HELP go_goroutines Number of goroutines that currently exist.
# TYPE go_goroutines gauge
go_goroutines 32
@@ -75,7 +77,26 @@ class OnlineParserTest {
# TYPE go_memstats_gc_sys_bytes gauge
go_memstats_gc_sys_bytes 4.614808e+06""";
InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
Map<String, MetricFamily> metricFamilyMap = OnlineParser.parseMetrics(inputStream);
assertNotNull(metricFamilyMap);
Map<String, MetricFamily> metricFamilyMap1 = OnlineParser.parseMetrics(inputStream);
Map<String, MetricFamily> metricFamilyMap2 = TextParser.textToMetricFamilies(str);
assertNotNull(metricFamilyMap1);
assertNotNull(metricFamilyMap2);
assertEquals(metricFamilyMap1.size(), metricFamilyMap2.size());
metricFamilyMap2.forEach((metricFamilyName, metricFamily2) -> {
if (!metricFamilyMap1.containsKey(metricFamilyName)) {
fail("parse failed, different result from two parser.");
}
MetricFamily metricFamily1 = metricFamilyMap1.get(metricFamilyName);
assertEquals(metricFamily1.getName(), metricFamily1.getName());
Set<Double> metricValueSet = metricFamily2.getMetricList().stream().map(MetricFamily.Metric::getValue).collect(Collectors.toSet());
metricFamily1.getMetricList().forEach(metric -> {
// this is for something different between two algorithms above, and both of them is current on this parsing behavior.
if (!(metric.getValue() == Double.POSITIVE_INFINITY || metric.getValue() == Double.NEGATIVE_INFINITY)) {
if (!metricValueSet.contains(metric.getValue())) {
fail();
}
}
});
});
}
}
@@ -0,0 +1,59 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.common.entity.dto.query;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* Metric History Range Query Data
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Schema(description = "Query Request Data")
public class DatasourceQuery {
@Schema(title = "Ref Id, unique id for the query")
private String refId;
@Schema(title = "datasource name")
private String datasource;
@Schema(title = "query expr, like prometheus query")
private String expr;
@Schema(title = "query expr type, like promql or sql or influxql")
private String exprType;
@Schema(title = "query range type, like range or instant")
private String timeType;
@Schema(title = "query range start time")
private Long start;
@Schema(title = "query range end time")
private Long end;
@Schema(title = "query time step, like 5m or 1h")
private String step;
}
@@ -18,14 +18,13 @@
package org.apache.hertzbeat.common.entity.dto.query;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;
import java.util.Map;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
import java.util.Map;
/**
* Metric History Range Query Data
*/
@@ -34,14 +33,36 @@ import java.util.Map;
@AllArgsConstructor
@NoArgsConstructor
@Schema(description = "Metric Query Data")
public class MetricQueryData {
public class DatasourceQueryData {
@Schema(title = "Ref Id, unique id for the query")
private String refId;
@Schema(title = "Metric Schema")
private MetricSchema schema;
@Schema(title = "query status code, 200 for success, other for error")
private Integer status;
@Schema(title = "query error message")
private String msg;
@Schema(title = "query result data frames")
private List<SchemaData> frames;
/**
* Schema Data
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public static final class SchemaData {
@Schema(title = "metrics row values, first is the timestamp-ts", example = "[[29,32,44],[32,34,true]]")
private List<List<Object>> values;
@Schema(title = "Metric Schema")
private MetricSchema schema;
@Schema(title = "metrics row values, first is the timestamp-ts", example = "[[29,32,44],[32,34,true]]")
private List<Object[]> data;
}
/**
* Metric Schema
*/
@@ -54,6 +75,9 @@ public class MetricQueryData {
@Schema(title = "Metrics Field")
private List<MetricField> fields;
@Schema(title = "This frame labels")
private Map<String, String> labels;
@Schema(title = "Meta Information")
private Map<String, String> meta;
}
@@ -75,8 +99,5 @@ public class MetricQueryData {
@Schema(title = "Field Unit: %, Mb, Kbps etc.")
private String unit;
@Schema(title = "Whether is a label")
private Boolean label;
}
}
@@ -49,4 +49,46 @@ public final class TimePeriodUtil {
return Duration.parse("PT" + tokenTime);
}
}
/**
* transform any timestamp to milliseconds
* @param timestamp timestamp
* @return milliseconds
*/
public static long normalizeToMilliseconds(Object timestamp) {
if (timestamp instanceof String timestampStr) {
// string type, may be second, millisecond or decimal second
// eg: "1672531199000", "1672531199", "1672531199.123"
if (timestampStr.contains(".")) {
// contains decimal point, parse as second timestamp
double seconds = Double.parseDouble(timestampStr);
return (long) (seconds * 1000);
} else {
// integer form, determine second or millisecond
long numericTimestamp = Long.parseLong(timestampStr);
return convertNumericTimestamp(numericTimestamp);
}
} else if (timestamp instanceof Number) {
// number eg Integer、Long、Double
if (timestamp instanceof Double || timestamp instanceof Float) {
// float type, treat as second timestamp
double seconds = ((Number) timestamp).doubleValue();
return (long) (seconds * 1000);
} else {
// integer type, directly determine second or millisecond
long numericTimestamp = ((Number) timestamp).longValue();
return convertNumericTimestamp(numericTimestamp);
}
} else {
throw new IllegalArgumentException("Not support this timestamp type: " + timestamp.getClass().getName());
}
}
private static long convertNumericTimestamp(long numericTimestamp) {
if (String.valueOf(numericTimestamp).length() <= 10) {
return numericTimestamp * 1000;
} else {
return numericTimestamp;
}
}
}
+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:
+3 -2
View File
@@ -29,10 +29,11 @@
<name>${project.artifactId}</name>
<dependencies>
<!-- common -->
<!-- collector basic -->
<dependency>
<groupId>org.apache.hertzbeat</groupId>
<artifactId>hertzbeat-common</artifactId>
<artifactId>hertzbeat-collector-basic</artifactId>
<version>2.0-SNAPSHOT</version>
</dependency>
<!-- spring -->
<dependency>
@@ -28,12 +28,12 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.collector.collect.prometheus.parser.MetricFamily;
import org.apache.hertzbeat.collector.collect.prometheus.parser.OnlineParser;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.dto.MetricFamily;
import org.apache.hertzbeat.common.entity.manager.Monitor;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.queue.CommonDataQueue;
import org.apache.hertzbeat.common.util.OnlineParser;
import org.apache.hertzbeat.common.util.SnowFlakeIdGenerator;
import org.apache.hertzbeat.push.dao.PushMonitorDao;
import org.apache.hertzbeat.push.service.PushGatewayService;
@@ -60,4 +60,8 @@ public interface WarehouseConstants {
String SQL = "sql";
String RANGE = "range";
String INSTANT = "instant";
}
@@ -0,0 +1,53 @@
/*
* 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.warehouse.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
import org.apache.hertzbeat.common.entity.dto.Message;
import org.apache.hertzbeat.common.entity.dto.query.DatasourceQuery;
import org.apache.hertzbeat.common.entity.dto.query.DatasourceQueryData;
import org.apache.hertzbeat.warehouse.service.DatasourceQueryService;
import org.springframework.beans.factory.annotation.Autowired;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Metrics Data Query API
*/
@RestController
@RequestMapping(produces = {APPLICATION_JSON_VALUE})
@Tag(name = "Metrics Data Query API")
public class DataQueryController {
@Autowired(required = false)
private DatasourceQueryService datasourceQueryService;
@PostMapping("/api/warehouse/query")
@Operation(summary = "Warehouse Query")
public ResponseEntity<Message<List<DatasourceQueryData>>> query(
@Parameter(description = "Query Expr") @RequestBody List<DatasourceQuery> queries) {
return ResponseEntity.ok(Message.success(datasourceQueryService.query(queries)));
}
}
@@ -1,78 +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.warehouse.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.apache.hertzbeat.common.entity.dto.Message;
import org.apache.hertzbeat.common.entity.dto.query.MetricQueryData;
import org.apache.hertzbeat.warehouse.service.MetricsDataQueryService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
/**
* Metrics Data Query API
*/
@RestController
@RequestMapping(produces = {APPLICATION_JSON_VALUE})
@Tag(name = "Metrics Data Query API")
public class MetricsDataQueryController {
@Autowired
private MetricsDataQueryService queryService;
@GetMapping("/api/warehouse/query")
@Operation(summary = "Query Real Time Metrics Data")
public ResponseEntity<Message<List<MetricQueryData>>> queryMetricsData(
@Parameter(description = "Query PromQL expr list", example = "cpu")
@RequestParam List<String> queries,
@Parameter(description = "Query type", example = "promql")
@RequestParam String type,
@Parameter(description = "Query timestamp", example = "1725854804451")
@RequestParam long time) {
return ResponseEntity.ok(Message.success(queryService.query(queries, type, time)));
}
@GetMapping("/api/warehouse/query/range")
@Operation(summary = "Query Range Metrics Data")
public ResponseEntity<Message<List<MetricQueryData>>> queryMetricsDataRange(
@Parameter(description = "Query PromQL expr list", example = "cpu")
@RequestParam List<String> queries,
@Parameter(description = "Query type", example = "promql")
@RequestParam String type,
@Parameter(description = "Query start timestamp", example = "1725854804451")
@RequestParam long start,
@Parameter(description = "Query end timestamp", example = "1733630804452")
@RequestParam long end,
@Parameter(description = "Query step", example = "4m")
@RequestParam String step
) {
return ResponseEntity.ok(Message.success(queryService.queryRange(queries, type, start, end, step)));
}
}
@@ -33,7 +33,9 @@ import org.springframework.web.client.RestTemplate;
@Slf4j
public class GreptimePromqlQueryExecutor extends PromqlQueryExecutor {
private static final String QUERY_PATH = "/v1/prometheus/api/v1/query";
private static final String QUERY_PATH = "/v1/prometheus";
private static final String Datasource = "Greptime";
private final GreptimeProperties greptimeProperties;
@@ -42,4 +44,10 @@ public class GreptimePromqlQueryExecutor extends PromqlQueryExecutor {
greptimeProperties.username(), greptimeProperties.password()));
this.greptimeProperties = greptimeProperties;
}
@Override
public String getDatasource() {
return Datasource;
}
}
@@ -20,10 +20,16 @@
package org.apache.hertzbeat.warehouse.db;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import org.apache.hertzbeat.common.constants.NetworkConstants;
import org.apache.hertzbeat.common.constants.SignConstants;
import org.apache.hertzbeat.common.entity.dto.query.MetricQueryData;
import org.apache.hertzbeat.common.entity.dto.query.DatasourceQuery;
import org.apache.hertzbeat.common.entity.dto.query.DatasourceQueryData;
import org.apache.hertzbeat.common.util.Base64Util;
import org.apache.hertzbeat.common.util.TimePeriodUtil;
import static org.apache.hertzbeat.warehouse.constants.WarehouseConstants.INSTANT;
import static org.apache.hertzbeat.warehouse.constants.WarehouseConstants.PROMQL;
import static org.apache.hertzbeat.warehouse.constants.WarehouseConstants.RANGE;
import org.apache.hertzbeat.warehouse.store.history.vm.PromQlQueryContent;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpEntity;
@@ -35,15 +41,12 @@ import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import java.net.URI;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import static org.apache.hertzbeat.warehouse.constants.WarehouseConstants.PROMQL;
/**
* abstract class for promql query executor
*/
@@ -51,11 +54,15 @@ import static org.apache.hertzbeat.warehouse.constants.WarehouseConstants.PROMQL
public abstract class PromqlQueryExecutor implements QueryExecutor {
private static final String supportQueryLanguage = PROMQL;
private static final String QUERY_RANGE_PATH = "/api/v1/query_range";
private static final String QUERY_PATH = "/api/v1/query";
protected static final String HTTP_QUERY_PARAM = "query";
protected static final String HTTP_TIME_PARAM = "time";
protected static final String HTTP_START_PARAM = "start";
protected static final String HTTP_END_PARAM = "end";
protected static final String HTTP_STEP_PARAM = "step";
private static final String INNER_KEY_TIME = "__ts__";
private static final String INNER_KEY_VALUE = "__value__";
private final RestTemplate restTemplate;
@@ -69,14 +76,15 @@ public abstract class PromqlQueryExecutor implements QueryExecutor {
/**
* record class for promql http connection
*/
protected record HttpPromqlProperties (
String url,
String username,
String password
){}
protected record HttpPromqlProperties(
String url,
String username,
String password
) {
}
protected List<Map<String, Object>> http_promql(Map<String, Object> params) {
// http run the promql query
@Override
public List<Map<String, Object>> execute(String queryString) {
List<Map<String, Object>> results = new LinkedList<>();
try {
HttpHeaders headers = new HttpHeaders();
@@ -86,14 +94,13 @@ public abstract class PromqlQueryExecutor implements QueryExecutor {
&& StringUtils.hasText(httpPromqlProperties.password())) {
String authStr = httpPromqlProperties.username() + ":" + httpPromqlProperties.password();
String encodedAuth = Base64Util.encode(authStr);
headers.add(HttpHeaders.AUTHORIZATION, NetworkConstants.BASIC + SignConstants.BLANK + encodedAuth);
headers.add(HttpHeaders.AUTHORIZATION, NetworkConstants.BASIC + SignConstants.BLANK + encodedAuth);
}
HttpEntity<Void> httpEntity = new HttpEntity<>(headers);
UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromHttpUrl(httpPromqlProperties.url);
for (Map.Entry<String, Object> entry : params.entrySet()) {
uriComponentsBuilder.queryParam(entry.getKey(), entry.getValue());
}
URI uri = uriComponentsBuilder.build(true).toUri();
UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromHttpUrl(httpPromqlProperties.url + QUERY_PATH);
uriComponentsBuilder.queryParam(HTTP_QUERY_PARAM, queryString);
URI uri = uriComponentsBuilder.build().toUri();
ResponseEntity<PromQlQueryContent> responseEntity = restTemplate.exchange(uri,
HttpMethod.GET, httpEntity, PromQlQueryContent.class);
if (responseEntity.getStatusCode().is2xxSuccessful()) {
@@ -126,41 +133,85 @@ public abstract class PromqlQueryExecutor implements QueryExecutor {
return results;
}
public MetricQueryData convertToMetricQueryData(Object object) {
MetricQueryData metricQueryData = new MetricQueryData();
@Override
public DatasourceQueryData query(DatasourceQuery datasourceQuery) {
DatasourceQueryData.DatasourceQueryDataBuilder queryDataBuilder = DatasourceQueryData.builder()
.refId(datasourceQuery.getRefId()).status(200);
try {
List<Map<String, Object>> metrics = (List<Map<String, Object>>) object;
// todo
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setAccept(List.of(MediaType.APPLICATION_JSON));
if (StringUtils.hasText(httpPromqlProperties.username())
&& StringUtils.hasText(httpPromqlProperties.password())) {
String authStr = httpPromqlProperties.username() + ":" + httpPromqlProperties.password();
String encodedAuth = new String(Base64.encodeBase64(authStr.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8);
headers.add(HttpHeaders.AUTHORIZATION, NetworkConstants.BASIC + " " + encodedAuth);
}
HttpEntity<Void> httpEntity = new HttpEntity<>(headers);
URI uri;
if (datasourceQuery.getTimeType().equals(RANGE)) {
uri = UriComponentsBuilder.fromHttpUrl(httpPromqlProperties.url() + QUERY_RANGE_PATH)
.queryParam(HTTP_QUERY_PARAM, datasourceQuery.getExpr())
.queryParam(HTTP_START_PARAM, datasourceQuery.getStart())
.queryParam(HTTP_END_PARAM, datasourceQuery.getEnd())
.queryParam(HTTP_STEP_PARAM, datasourceQuery.getStep())
.build().toUri();
} else if (datasourceQuery.getTimeType().equals(INSTANT)) {
uri = UriComponentsBuilder.fromHttpUrl(httpPromqlProperties.url() + QUERY_PATH)
.queryParam(HTTP_QUERY_PARAM, datasourceQuery.getExpr())
.build().toUri();
} else {
throw new IllegalArgumentException(String.format("no such time type for query id {}.", datasourceQuery.getRefId()));
}
ResponseEntity<PromQlQueryContent> responseEntity = restTemplate.exchange(uri, HttpMethod.GET, httpEntity,
PromQlQueryContent.class);
if (responseEntity.getStatusCode().is2xxSuccessful()) {
log.debug("query metrics data from promql http api success. {}", uri);
if (responseEntity.getBody() != null && responseEntity.getBody().getData() != null
&& responseEntity.getBody().getData().getResult() != null) {
List<PromQlQueryContent.ContentData.Content> contents = responseEntity.getBody().getData().getResult();
List<DatasourceQueryData.SchemaData> schemaDataList = new LinkedList<>();
for (PromQlQueryContent.ContentData.Content content : contents) {
DatasourceQueryData.MetricSchema.MetricSchemaBuilder schemaBuilder = DatasourceQueryData.MetricSchema
.builder().fields(List.of(
// todo: unit?
DatasourceQueryData.MetricField.builder().name(INNER_KEY_TIME)
.type("time").build(),
DatasourceQueryData.MetricField.builder().name(INNER_KEY_VALUE)
.type("number").build()
)).labels(content.getMetric());
List<Object[]> values;
if (datasourceQuery.getTimeType().equals(RANGE)) {
values = content.getValues();
}
else {
values = List.<Object[]>of(content.getValue());
}
values.forEach(objects -> {
objects[0] = TimePeriodUtil.normalizeToMilliseconds(objects[0]);
});
DatasourceQueryData.SchemaData.SchemaDataBuilder schemaData = DatasourceQueryData.SchemaData.builder()
.schema(schemaBuilder.build()).data(values);
schemaDataList.add(schemaData.build());
}
queryDataBuilder.frames(schemaDataList);
}
} else {
log.error("query metrics data from victoria-metrics failed. {}", responseEntity);
queryDataBuilder.msg("query metrics data from victoria-metrics failed. ");
queryDataBuilder.status(responseEntity.getStatusCode().value());
}
} catch (Exception e) {
log.error("converting to metric query data failed.");
log.error("query metrics data from victoria-metrics error. {}.", e.getMessage(), e);
queryDataBuilder.msg("query metrics data from victoria-metrics error: " + e.getMessage());
queryDataBuilder.status(400);
}
return metricQueryData;
return queryDataBuilder.build();
}
public List<Map<String, Object>> execute(String queryString) {
Map<String, Object> params = new HashMap<>();
params.put(HTTP_QUERY_PARAM, URLEncoder.encode(queryString, StandardCharsets.UTF_8));
return http_promql(params);
}
public List<Map<String, Object>> query(String queryString, long time) {
Map<String, Object> params = new HashMap<>();
params.put(HTTP_QUERY_PARAM, URLEncoder.encode(queryString, StandardCharsets.UTF_8));
params.put(HTTP_TIME_PARAM, time);
return http_promql(params);
}
public List<Map<String, Object>> query_range(String queryString, long start, long end, String step) {
Map<String, Object> params = new HashMap<>();
params.put(HTTP_QUERY_PARAM, URLEncoder.encode(queryString, StandardCharsets.UTF_8));
params.put(HTTP_START_PARAM, start);
params.put(HTTP_END_PARAM, end);
params.put(HTTP_STEP_PARAM, step);
return http_promql(params);
}
public boolean support(String datasource) {
return supportQueryLanguage.equals(datasource);
@Override
public boolean support(String queryLanguage) {
return StringUtils.hasText(queryLanguage) && queryLanguage.equalsIgnoreCase(supportQueryLanguage);
}
}
@@ -17,7 +17,8 @@
package org.apache.hertzbeat.warehouse.db;
import org.apache.hertzbeat.common.entity.dto.query.MetricQueryData;
import org.apache.hertzbeat.common.entity.dto.query.DatasourceQuery;
import org.apache.hertzbeat.common.entity.dto.query.DatasourceQueryData;
import java.util.List;
import java.util.Map;
@@ -27,13 +28,11 @@ import java.util.Map;
*/
public interface QueryExecutor {
MetricQueryData convertToMetricQueryData(Object object);
List<Map<String, Object>> execute(String query);
List<Map<String, Object>> query(String query, long time);
List<Map<String, Object>> query_range(String query, long start, long end, String step);
boolean support(String datasource);
DatasourceQueryData query(DatasourceQuery datasourceQuery);
String getDatasource();
boolean support(String queryLanguage);
}
@@ -20,13 +20,15 @@
package org.apache.hertzbeat.warehouse.db;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.common.entity.dto.query.MetricQueryData;
import org.apache.hertzbeat.common.entity.dto.query.DatasourceQuery;
import org.apache.hertzbeat.common.entity.dto.query.DatasourceQueryData;
import static org.apache.hertzbeat.warehouse.constants.WarehouseConstants.SQL;
import org.springframework.util.StringUtils;
import java.util.List;
import java.util.Map;
import static org.apache.hertzbeat.warehouse.constants.WarehouseConstants.SQL;
/**
* abstract class for sql query executor
*/
@@ -40,28 +42,19 @@ public abstract class SqlQueryExecutor implements QueryExecutor {
*/
protected record ConnectorSqlProperties () {}
protected abstract List<Map<String, Object>> do_sql(Map<String, Object> params);
public MetricQueryData convertToMetricQueryData(Object object) {
MetricQueryData metricQueryData = new MetricQueryData();
try {
List<Map<String, Object>> metrics = (List<Map<String, Object>>) object;
// todo
} catch (Exception e) {
log.error("converting to metric query data failed.");
}
return metricQueryData;
@Override
public List<Map<String, Object>> execute(String query) {
return null;
}
public abstract List<Map<String, Object>> execute(String query);
public abstract List<Map<String, Object>> query(String query, long time);
public abstract List<Map<String, Object>> query_range(String query, long start, long end, String step);
public boolean support(String datasource) {
return supportQueryLanguage.equals(datasource);
@Override
public DatasourceQueryData query(DatasourceQuery datasourceQuery) {
return null;
}
}
@Override
public boolean support(String queryLanguage) {
return StringUtils.hasText(queryLanguage) && queryLanguage.equalsIgnoreCase(supportQueryLanguage);
}
}
@@ -31,15 +31,19 @@ import org.springframework.web.client.RestTemplate;
@ConditionalOnProperty(prefix = "warehouse.store.victoria-metrics", name = "enabled", havingValue = "true")
@Slf4j
public class VictoriaMetricsQueryExecutor extends PromqlQueryExecutor {
private static final String QUERY_PATH = "/api/v1/query";
private static final String Datasource = "VictoriaMetrics";
private final VictoriaMetricsProperties victoriaMetricsProp;
public VictoriaMetricsQueryExecutor(VictoriaMetricsProperties victoriaMetricsProp, RestTemplate restTemplate) {
super(restTemplate, new HttpPromqlProperties(victoriaMetricsProp.url() + QUERY_PATH,
super(restTemplate, new HttpPromqlProperties(victoriaMetricsProp.url(),
victoriaMetricsProp.username(), victoriaMetricsProp.password()));
this.victoriaMetricsProp = victoriaMetricsProp;
}
@Override
public String getDatasource() {
return Datasource;
}
}
@@ -17,30 +17,19 @@
package org.apache.hertzbeat.warehouse.service;
import org.apache.hertzbeat.common.entity.dto.query.MetricQueryData;
import java.util.List;
import org.apache.hertzbeat.common.entity.dto.query.DatasourceQuery;
import org.apache.hertzbeat.common.entity.dto.query.DatasourceQueryData;
/**
* metrics data query service
*/
public interface MetricsDataQueryService {
public interface DatasourceQueryService {
/**
* Query metrics data
* @param queries query expr
* @param time time
* @return data
*/
List<MetricQueryData> query(List<String> queries, String queryType, long time);
/**
* Query metrics data range
* @param queries query expr
* @param start start
* @param end end
* @param step step
* @return data
*/
List<MetricQueryData> queryRange(List<String> queries, String queryType, long start, long end, String step);
List<DatasourceQueryData> query(List<DatasourceQuery> queries);
}
@@ -0,0 +1,60 @@
/*
* 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.warehouse.service.impl;
import org.apache.hertzbeat.common.entity.dto.query.DatasourceQuery;
import org.apache.hertzbeat.common.entity.dto.query.DatasourceQueryData;
import org.apache.hertzbeat.warehouse.db.QueryExecutor;
import org.apache.hertzbeat.warehouse.service.DatasourceQueryService;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* datasource query service impl
*/
@Service
public class DatasourceQueryServiceImpl implements DatasourceQueryService {
Map<String, QueryExecutor> executorMap;
DatasourceQueryServiceImpl(List<QueryExecutor> executors) {
executorMap = executors.stream().collect(Collectors.toMap(QueryExecutor::getDatasource, executor -> executor));
}
@Override
public List<DatasourceQueryData> query(List<DatasourceQuery> queries) {
if (queries == null) {
throw new IllegalArgumentException("No query found");
}
List<DatasourceQueryData> datasourceQueryDataList = new ArrayList<>();
for (DatasourceQuery datasourceQuery : queries) {
QueryExecutor executor = executorMap.get(datasourceQuery.getDatasource());
if (executor == null) {
throw new IllegalArgumentException("Unsupported datasource: " + datasourceQuery.getDatasource());
}
datasourceQueryDataList.add(executor.query(datasourceQuery));
}
return datasourceQueryDataList;
}
}
@@ -1,67 +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.warehouse.service.impl;
import org.apache.hertzbeat.common.entity.dto.query.MetricQueryData;
import org.apache.hertzbeat.warehouse.db.QueryExecutor;
import org.apache.hertzbeat.warehouse.service.MetricsDataQueryService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class MetricsDataQueryServiceImpl implements MetricsDataQueryService {
@Autowired(required = false)
List<QueryExecutor> executors;
@Override
public List<MetricQueryData> query(List<String> queries, String queryType, long time) {
if (queries == null || executors.isEmpty()) {
throw new IllegalArgumentException("No query executor found");
}
QueryExecutor executor = executors.stream().filter(e -> e.support(queryType)).findFirst().orElse(null);
if (executor == null) {
throw new IllegalArgumentException("Unsupported datasource: " + queryType);
}
List<MetricQueryData> metricQueryDataList = new ArrayList<>();
for (String query : queries) {
metricQueryDataList.add(executor.convertToMetricQueryData(executor.query(query, time)));
}
return metricQueryDataList;
}
@Override
public List<MetricQueryData> queryRange(List<String> queries, String queryType, long start, long end, String step) {
if (queries == null || executors.isEmpty()) {
throw new IllegalArgumentException("No query executor found");
}
QueryExecutor executor = executors.stream().filter(e -> e.support(queryType)).findFirst().orElse(null);
if (executor == null) {
throw new IllegalArgumentException("Unsupported datasource: " + queryType);
}
List<MetricQueryData> metricQueryDataList = new ArrayList<>();
for (String query : queries) {
metricQueryDataList.add(executor.convertToMetricQueryData(executor.query_range(query, start, end, step)));
}
return metricQueryDataList;
}
}
+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": "觸發時間",