mirror of
https://github.com/dromara/hertzbeat.git
synced 2026-09-17 18:19:02 +00:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b2d586f61 | ||
|
|
3d14277019 | ||
|
|
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());
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -98,9 +98,9 @@ public abstract class PromqlQueryExecutor implements QueryExecutor {
|
||||
}
|
||||
HttpEntity<Void> httpEntity = new HttpEntity<>(headers);
|
||||
|
||||
UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromHttpUrl(httpPromqlProperties.url);
|
||||
UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromHttpUrl(httpPromqlProperties.url + QUERY_PATH);
|
||||
uriComponentsBuilder.queryParam(HTTP_QUERY_PARAM, queryString);
|
||||
URI uri = uriComponentsBuilder.build(true).toUri();
|
||||
URI uri = uriComponentsBuilder.build().toUri();
|
||||
ResponseEntity<PromQlQueryContent> responseEntity = restTemplate.exchange(uri,
|
||||
HttpMethod.GET, httpEntity, PromQlQueryContent.class);
|
||||
if (responseEntity.getStatusCode().is2xxSuccessful()) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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