mirror of
https://github.com/dromara/hertzbeat.git
synced 2026-09-17 18:19:02 +00:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0af35fb551 | ||
|
|
81ad7d51b6 | ||
|
|
5813d67cd5 | ||
|
|
db471bb7ee | ||
|
|
808a2938e7 | ||
|
|
97b1e906c5 | ||
|
|
9fa5b17fe4 | ||
|
|
9700161fdd | ||
|
|
f578df1deb | ||
|
|
eb58bb76f2 |
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
+30
-5
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
+36
-10
@@ -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
|
||||
|
||||
+26
-34
@@ -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);
|
||||
|
||||
-432
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
-252
@@ -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;
|
||||
}
|
||||
}
|
||||
-52
@@ -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;
|
||||
}
|
||||
}
|
||||
+13
-24
@@ -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();
|
||||
|
||||
+1
-1
@@ -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;
|
||||
+33
-44
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+2
-2
@@ -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
|
||||
*/
|
||||
|
||||
+6
-6
@@ -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);
|
||||
|
||||
-79
@@ -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());
|
||||
}
|
||||
}
|
||||
+30
-9
@@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
+59
@@ -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;
|
||||
}
|
||||
+32
-11
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -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:
|
||||
@@ -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>
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
+4
@@ -60,4 +60,8 @@ public interface WarehouseConstants {
|
||||
|
||||
String SQL = "sql";
|
||||
|
||||
String RANGE = "range";
|
||||
|
||||
String INSTANT = "instant";
|
||||
|
||||
}
|
||||
|
||||
+53
@@ -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)));
|
||||
}
|
||||
}
|
||||
-78
@@ -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)));
|
||||
}
|
||||
}
|
||||
+9
-1
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+99
-48
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+7
-8
@@ -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);
|
||||
}
|
||||
|
||||
+17
-24
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+7
-3
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+5
-16
@@ -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);
|
||||
}
|
||||
+60
@@ -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;
|
||||
}
|
||||
}
|
||||
-67
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -91,6 +91,7 @@
|
||||
<module>hertzbeat-log</module>
|
||||
<module>hertzbeat-e2e</module>
|
||||
<module>hertzbeat-base</module>
|
||||
<module>hertzbeat-mcp</module>
|
||||
</modules>
|
||||
|
||||
<properties>
|
||||
|
||||
Submodule script/helm/hertzbeat-helm-chart updated: 9ce6a771c1...bdb57c002b
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "トリガー時間",
|
||||
|
||||
+771
-293
File diff suppressed because it is too large
Load Diff
@@ -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": "触发时间",
|
||||
|
||||
@@ -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": "觸發時間",
|
||||
|
||||
Reference in New Issue
Block a user