Compare commits

..
Author SHA1 Message Date
vinci 8b2d586f61 fix UriComponentsBuilder in PromqlQueryExecutor 2025-04-14 19:55:19 +08:00
vinci 3d14277019 fix UriComponentsBuilder in PromqlQueryExecutor 2025-04-14 19:53:28 +08:00
tomsun28 9700161fdd [doc] add new contributors to wall (#3243)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-04-14 16:40:24 +08:00
f578df1deb [feat] enable label-based filtering and selection for monitoring thresholds (#3223)
Co-authored-by: aias00 <liuhongyu@apache.org>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-04-14 14:26:52 +08:00
eb58bb76f2 [feature] switch to prometheus online parser & add query datasource (#3215)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
Signed-off-by: leo <1552443053@qq.com>
Co-authored-by: Ceilzcx <1758619238@qq.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: aias00 <liuhongyu@apache.org>
2025-04-14 14:08:11 +08:00
16 changed files with 1217 additions and 367 deletions
+36
View File
@@ -2361,6 +2361,42 @@
"contributions": [
"code"
]
},
{
"login": "PengJingzhao",
"name": "彭镜肇",
"avatar_url": "https://avatars.githubusercontent.com/u/97368949?v=4",
"profile": "https://github.com/PengJingzhao",
"contributions": [
"code"
]
},
{
"login": "gagaradio",
"name": "Walter Jia",
"avatar_url": "https://avatars.githubusercontent.com/u/18532370?v=4",
"profile": "https://github.com/gagaradio",
"contributions": [
"code"
]
},
{
"login": "boyucjz",
"name": "boyucjz",
"avatar_url": "https://avatars.githubusercontent.com/u/18730041?v=4",
"profile": "https://github.com/boyucjz",
"contributions": [
"code"
]
},
{
"login": "Cyanty",
"name": "Cyanty",
"avatar_url": "https://avatars.githubusercontent.com/u/153884653?v=4",
"profile": "https://github.com/Cyanty",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,
+4
View File
@@ -516,6 +516,10 @@ Thanks to these wonderful people, welcome to join us:
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sarthakeash"><img src="https://avatars.githubusercontent.com/u/74091160?v=4?s=100" width="100px;" alt="Sarthak Arora"/><br /><sub><b>Sarthak Arora</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=sarthakeash" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/PengJingzhao"><img src="https://avatars.githubusercontent.com/u/97368949?v=4?s=100" width="100px;" alt="彭镜肇"/><br /><sub><b>彭镜肇</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=PengJingzhao" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/gagaradio"><img src="https://avatars.githubusercontent.com/u/18532370?v=4?s=100" width="100px;" alt="Walter Jia"/><br /><sub><b>Walter Jia</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=gagaradio" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/boyucjz"><img src="https://avatars.githubusercontent.com/u/18730041?v=4?s=100" width="100px;" alt="boyucjz"/><br /><sub><b>boyucjz</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=boyucjz" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Cyanty"><img src="https://avatars.githubusercontent.com/u/153884653?v=4?s=100" width="100px;" alt="Cyanty"/><br /><sub><b>Cyanty</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Cyanty" title="Code">💻</a></td>
</tr>
</tbody>
</table>
+4
View File
@@ -515,6 +515,10 @@ Thanks these wonderful people, welcome to join us:
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sarthakeash"><img src="https://avatars.githubusercontent.com/u/74091160?v=4?s=100" width="100px;" alt="Sarthak Arora"/><br /><sub><b>Sarthak Arora</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=sarthakeash" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/PengJingzhao"><img src="https://avatars.githubusercontent.com/u/97368949?v=4?s=100" width="100px;" alt="彭镜肇"/><br /><sub><b>彭镜肇</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=PengJingzhao" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/gagaradio"><img src="https://avatars.githubusercontent.com/u/18532370?v=4?s=100" width="100px;" alt="Walter Jia"/><br /><sub><b>Walter Jia</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=gagaradio" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/boyucjz"><img src="https://avatars.githubusercontent.com/u/18730041?v=4?s=100" width="100px;" alt="boyucjz"/><br /><sub><b>boyucjz</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=boyucjz" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Cyanty"><img src="https://avatars.githubusercontent.com/u/153884653?v=4?s=100" width="100px;" alt="Cyanty"/><br /><sub><b>Cyanty</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Cyanty" title="Code">💻</a></td>
</tr>
</tbody>
</table>
@@ -17,11 +17,14 @@
package org.apache.hertzbeat.alert.calculate;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -62,12 +65,14 @@ public class RealTimeAlertCalculator {
private static final String KEY_PRIORITY = "__priority__";
private static final String KEY_CODE = "__code__";
private static final String KEY_AVAILABLE = "__available__";
private static final String KEY_LABELS = "__labels__";
private static final String UP = "up";
private static final String DOWN = "down";
private static final String KEY_ROW = "__row__";
private static final Pattern APP_PATTERN = Pattern.compile("equals\\(__app__,\"([^\"]+)\"\\)");
private static final Pattern AVAILABLE_PATTERN = Pattern.compile("equals\\(__available__,\"([^\"]+)\"\\)");
private static final Pattern LABEL_PATTERN = Pattern.compile("contains\\(__labels__,\\s*\"([^\"]+)\"\\)");
private static final Pattern INSTANCE_PATTERN = Pattern.compile("equals\\(__instance__,\"(\\d+)\"\\)");
private static final Pattern METRICS_PATTERN = Pattern.compile("equals\\(__metrics__,\"([^\"]+)\"\\)");
@@ -136,8 +141,8 @@ public class RealTimeAlertCalculator {
Map<String, String> labels = metricsData.getLabels();
Map<String, String> annotations = metricsData.getAnnotations();
List<AlertDefine> thresholds = this.alertDefineService.getRealTimeAlertDefines();
// Filter thresholds by app, metrics and instance
thresholds = filterThresholdsByAppAndMetrics(thresholds, app, metrics, instance, priority);
// Filter thresholds by app, metrics, labels and instance
thresholds = filterThresholdsByAppAndMetrics(thresholds, app, metrics, labels, instance, priority);
if (thresholds.isEmpty()) {
return;
}
@@ -149,6 +154,7 @@ public class RealTimeAlertCalculator {
commonContext.put(KEY_PRIORITY, priority);
commonContext.put(KEY_CODE, code);
commonContext.put(KEY_METRICS, metrics);
commonContext.put(KEY_LABELS, String.join(",", kvLabelsToKvStringSet(labels)));
if (priority == 0) {
commonContext.put(KEY_AVAILABLE, metricsData.getCode() == CollectRep.Code.SUCCESS ? UP : DOWN);
}
@@ -264,7 +270,7 @@ public class RealTimeAlertCalculator {
* @param priority Current priority
* @return Filtered alert definitions
*/
private List<AlertDefine> filterThresholdsByAppAndMetrics(List<AlertDefine> thresholds, String app, String metrics, String instance, int priority) {
private List<AlertDefine> filterThresholdsByAppAndMetrics(List<AlertDefine> thresholds, String app, String metrics, Map<String, String> labels, String instance, int priority) {
return thresholds.stream()
.filter(define -> {
if (StringUtils.isBlank(define.getExpr())) {
@@ -294,19 +300,29 @@ public class RealTimeAlertCalculator {
// Extract and check instance - optional with multiple values
Matcher instanceMatcher = INSTANCE_PATTERN.matcher(expr);
// If no instance specified in expr, accept all instances
if (!instanceMatcher.find()) {
Matcher labelMatcher = LABEL_PATTERN.matcher(expr);
// If no instance and instance labels specified in expr, accept all instances
if (!instanceMatcher.find() && !labelMatcher.find()) {
return true;
}
// Reset matcher to check all instances
instanceMatcher.reset();
labelMatcher.reset();
// If instances specified, current instance must match one of them
while (instanceMatcher.find()) {
if (Objects.equals(instance, instanceMatcher.group(1))) {
return true;
}
}
// If instance labels specified, current instance must match one of them
Set<String> labelKvStringSet = kvLabelsToKvStringSet(labels);
while (labelMatcher.find()) {
String label = labelMatcher.group(1);
if (labelKvStringSet.contains(label)) {
return true;
}
}
return false;
})
.collect(Collectors.toList());
@@ -417,4 +433,13 @@ public class RealTimeAlertCalculator {
return Arrays.hashCode(keyList.toArray(new String[0])) + "-"
+ Arrays.hashCode(valueList.toArray(new String[0]));
}
private Set<String> kvLabelsToKvStringSet(Map<String, String> labels) {
if (labels == null || labels.isEmpty()) {
return Collections.singleton("");
}
return labels.entrySet().stream()
.map(item -> item.getKey() + ":" + item.getValue())
.collect(Collectors.toSet());
}
}
@@ -98,9 +98,9 @@ public abstract class PromqlQueryExecutor implements QueryExecutor {
}
HttpEntity<Void> httpEntity = new HttpEntity<>(headers);
UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromHttpUrl(httpPromqlProperties.url);
UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromHttpUrl(httpPromqlProperties.url + QUERY_PATH);
uriComponentsBuilder.queryParam(HTTP_QUERY_PARAM, queryString);
URI uri = uriComponentsBuilder.build(true).toUri();
URI uri = uriComponentsBuilder.build().toUri();
ResponseEntity<PromQlQueryContent> responseEntity = restTemplate.exchange(uri,
HttpMethod.GET, httpEntity, PromQlQueryContent.class);
if (responseEntity.getStatusCode().is2xxSuccessful()) {
+4
View File
@@ -454,6 +454,10 @@ export default function () {
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sarthakeash"><img src="https://avatars.githubusercontent.com/u/74091160?v=4?s=100" width="100px;" alt="Sarthak Arora"/><br /><sub><b>Sarthak Arora</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=sarthakeash" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/PengJingzhao"><img src="https://avatars.githubusercontent.com/u/97368949?v=4?s=100" width="100px;" alt="彭镜肇"/><br /><sub><b>彭镜肇</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=PengJingzhao" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/gagaradio"><img src="https://avatars.githubusercontent.com/u/18532370?v=4?s=100" width="100px;" alt="Walter Jia"/><br /><sub><b>Walter Jia</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=gagaradio" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/boyucjz"><img src="https://avatars.githubusercontent.com/u/18730041?v=4?s=100" width="100px;" alt="boyucjz"/><br /><sub><b>boyucjz</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=boyucjz" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Cyanty"><img src="https://avatars.githubusercontent.com/u/153884653?v=4?s=100" width="100px;" alt="Cyanty"/><br /><sub><b>Cyanty</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Cyanty" title="Code">💻</a></td>
</tr>
</tbody>
</table>
@@ -586,40 +586,111 @@
nzWidth="60%"
[nzOkLoading]="isConnectModalOkLoading"
>
<nz-transfer
*nzModalContent
[nzDataSource]="transferData"
nzShowSearch="true"
nzShowSelectAll="false"
[nzRenderList]="[renderList, renderList]"
(nzChange)="change($event)"
style="overflow-x: scroll"
>
<ng-template
#renderList
let-items
let-direction="direction"
let-stat="stat"
let-onItemSelectAll="onItemSelectAll"
let-onItemSelect="onItemSelect"
>
<nz-table #t [nzData]="$asTransferItems(items)" nzSize="small">
<thead>
<tr>
<th [nzChecked]="stat.checkAll" [nzIndeterminate]="stat.checkHalf" (nzCheckedChange)="onItemSelectAll($event)"></th>
<th *ngIf="direction == 'left'">{{ 'alert.setting.connect.left' | i18n }}</th>
<th *ngIf="direction == 'right'">{{ 'alert.setting.connect.right' | i18n }}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let data of t.data" (click)="onItemSelect(data)">
<td [nzChecked]="!!data.checked" (nzCheckedChange)="onItemSelect(data)"></td>
<td>{{ data.title }}</td>
</tr>
</tbody>
</nz-table>
</ng-template>
</nz-transfer>
<div *nzModalContent>
<nz-card [nzTitle]="'alert.setting.connect.instance' | i18n">
<nz-transfer
[nzDataSource]="filteredTransferData"
nzShowSelectAll="false"
[nzRenderList]="[renderList, renderList]"
(nzChange)="change($event)"
style="overflow-x: scroll"
>
<ng-template
#renderList
let-items
let-direction="direction"
let-stat="stat"
let-onItemSelectAll="onItemSelectAll"
let-onItemSelect="onItemSelect"
>
<div>
<div class="filter-container">
<input
type="text"
nz-input
[placeholder]="'alert.setting.filter.name' | i18n"
[ngModel]="getSearchValue(direction)"
(ngModelChange)="onSearchValueChange($event, direction)"
style="width: 94%"
/>
</div>
<div class="filter-container">
<nz-select
[ngModel]="getFilterLabels(direction)"
(ngModelChange)="onFilterLabelsChange($event, direction)"
name="groupLabels"
nzMode="tags"
[nzTokenSeparators]="[',']"
(keydown.enter)="$event.preventDefault()"
nzAllowClear
[nzPlaceHolder]="'alert.setting.filter.labels' | i18n"
style="width: 94%"
>
<nz-option *ngFor="let label of getMonitorLabels(direction)" [nzLabel]="label" [nzValue]="label"></nz-option>
</nz-select>
</div>
</div>
<nz-table #t [nzData]="$asTransferItems(items)" nzSize="small">
<thead>
<tr>
<th [nzChecked]="stat.checkAll" [nzIndeterminate]="stat.checkHalf" (nzCheckedChange)="onItemSelectAll($event)"></th>
<th *ngIf="direction == 'left'">{{ 'alert.setting.connect.left' | i18n }}</th>
<th *ngIf="direction == 'right'">{{ 'alert.setting.connect.right' | i18n }}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let data of t.data" (click)="onItemSelect(data)">
<td [nzChecked]="!!data.checked" (nzCheckedChange)="onItemSelect(data)"></td>
<td>{{ data.title }}</td>
</tr>
</tbody>
</nz-table>
</ng-template>
</nz-transfer>
</nz-card>
<nz-card [nzTitle]="'alert.setting.connect.label' | i18n">
<div class="label-input-container">
@for (label of selectedLabels; track label) {
<nz-tag [nzMode]="'closeable'" [nzColor]="'geekblue'" (nzOnClose)="handleDeleteLabel(label)">
{{ label }}
</nz-tag>
}
<nz-tag *ngIf="!labelInputVisible" nzNoAnimation (click)="showLabelInput()">
<i nz-icon nzType="plus"></i>
{{ 'alert.setting.connect.label.input' | i18n }}
</nz-tag>
<input
*ngIf="labelInputVisible"
#labelInputElement
nz-input
nzSize="small"
type="text"
[(ngModel)]="inputLabelValue"
style="width: 78px"
(blur)="handleLabelInputConfirm()"
(keydown.enter)="handleLabelInputConfirm()"
/>
</div>
<div>
<nz-empty
*ngIf="monitorDataByLabel.length <= 0"
nzNotFoundImage="simple"
[nzNotFoundContent]="'alert.setting.connect.label.empty' | i18n"
></nz-empty>
<nz-list nzBordered *ngIf="monitorDataByLabel.length > 0">
<div class="scroll-container">
<nz-list-item *ngFor="let item of monitorDataByLabel">
<span nz-typography>{{ item.title }}</span>
<span
><nz-tag *ngFor="let label of item.labels">{{ label }}</nz-tag></span
>
</nz-list-item>
</div>
</nz-list>
</div>
</nz-card>
</div>
</nz-modal>
<!-- export alert definitions pop-up box -->
@@ -1,12 +1,11 @@
@import '@delon/theme/index';
@import '../../../../styles/theme.less';
::ng-deep {
.row {
margin-top: 6px;
.ruleset {
border: 1px solid #CCC;
border: 1px solid #ccc;
}
}
@@ -20,14 +19,14 @@
}
.ruleset-invalid {
border: none!important;
border: none !important;
.ruleset {
border: 1px solid #ff4d4f !important;
}
>p {
margin: 0!important;
> p {
margin: 0 !important;
color: #ff4d4f !important;
}
}
@@ -44,7 +43,7 @@
min-width: 300px;
overflow-x: auto;
padding: 6px 8px;
border: 1px solid #CCC;
border: 1px solid #ccc;
.q-rule-content {
flex: 1;
@@ -350,4 +349,19 @@
min-width: auto;
}
}
.filter-container {
display: flex;
justify-content: center;
margin-bottom: 8px;
}
.label-input-container {
margin-bottom: 10px;
}
.scroll-container {
height: 200px;
overflow-y: auto;
}
}
@@ -17,7 +17,7 @@
* under the License.
*/
import { Component, HostListener, Inject, OnInit, ViewChild } from '@angular/core';
import { Component, ElementRef, HostListener, Inject, OnInit, ViewChild } from '@angular/core';
import { AbstractControl, FormBuilder, FormControl, NgForm, ValidationErrors } from '@angular/forms';
import { I18NService } from '@core';
import { ALAIN_I18N_TOKEN } from '@delon/theme';
@@ -104,6 +104,7 @@ export class AlertSettingComponent implements OnInit {
templateEnvVars = [
{ name: '${__instance__}', description: 'alert.setting.template.vars.instance' },
{ name: '${__labels__}', description: 'alert.setting.template.vars.labels' },
{ name: '${__instancename__}', description: 'alert.setting.template.vars.instance-name' },
{ name: '${__instancehost__}', description: 'alert.setting.template.vars.instance-host' },
{ name: '${__app__}', description: 'alert.setting.template.vars.app' },
@@ -245,6 +246,7 @@ export class AlertSettingComponent implements OnInit {
this.severity = '';
this.userExpr = '';
this.selectedMonitorIds = new Set<number>();
this.selectedLabels = new Set<string>();
// Set default period for periodic alert
if (type === 'periodic') {
this.define.period = 300;
@@ -497,6 +499,7 @@ export class AlertSettingComponent implements OnInit {
this.cascadeValues = this.exprToCascadeValues(this.define.expr);
this.userExpr = this.exprToUserExpr(this.define.expr);
this.parseMonitorIdsFromExpr(this.define.expr);
this.parseLabelFromExpr(this.define.expr);
this.cascadeOnChange(this.cascadeValues);
// Wait for cascade values to be set
setTimeout(() => {
@@ -943,8 +946,21 @@ export class AlertSettingComponent implements OnInit {
// start -- associate alert definition and monitoring model
isConnectModalVisible = false;
isConnectModalOkLoading = false;
filteredTransferData: TransferItem[] = [];
transferData: TransferItem[] = [];
selectedMonitorIds = new Set<number>();
leftMonitorLabels: Set<string> = new Set(); // All available labels on the left side
rightMonitorLabels: Set<string> = new Set(); // All available labels on the right side
leftSearchValue = '';
rightSearchValue = '';
leftFilterLabels: string[] = []; // Labels used for filtering on the left side
rightFilterLabels: string[] = []; // Labels used for filtering on the right side
labelInputVisible = false;
selectedLabels: Set<string> = new Set(); // Selected labels
inputLabelValue = '';
labelInputElement!: ElementRef<HTMLInputElement>;
monitorDataByLabel: any[] = [];
$asTransferItems = (data: unknown): TransferItem[] => data as TransferItem[];
onConnectModalCancel() {
this.isConnectModalVisible = false;
@@ -979,7 +995,148 @@ export class AlertSettingComponent implements OnInit {
}
return e;
});
this.updateMonitorLabel();
}
handleDeleteLabel(removedLabel: string): void {
this.selectedLabels.delete(removedLabel);
this.showMonitorByLabel();
}
showLabelInput(): void {
this.labelInputVisible = true;
setTimeout(() => {
this.labelInputElement?.nativeElement.focus();
}, 10);
}
handleLabelInputConfirm(): void {
if (this.inputLabelValue && !this.selectedLabels.has(this.inputLabelValue)) {
this.selectedLabels.add(this.inputLabelValue);
}
this.showMonitorByLabel();
this.inputLabelValue = '';
this.labelInputVisible = false;
}
showMonitorByLabel(): void {
const monitorsByLabel = this.transferData.filter(item => item.labels.some((label: string) => this.selectedLabels.has(label)));
this.monitorDataByLabel = monitorsByLabel.map(item => {
return {
key: item.key,
title: item.title,
description: item.description,
labels: item.labels
};
});
}
onFilterLabelsChange(newLabels: string[], direction: string): void {
if (direction == 'left') {
this.leftFilterLabels = newLabels;
} else {
this.rightFilterLabels = newLabels;
}
this.handleSearch(direction);
}
onSearchValueChange(value: string, direction: string): void {
if (direction === 'left') {
this.leftSearchValue = value;
} else {
this.rightSearchValue = value;
}
this.handleSearch(direction);
}
getFilterLabels(direction: string): string[] {
return direction === 'left' ? this.leftFilterLabels : this.rightFilterLabels;
}
getSearchValue(direction: string): string {
return direction === 'left' ? this.leftSearchValue : this.rightSearchValue;
}
getMonitorLabels(direction: string): Set<string> {
return direction === 'left' ? this.leftMonitorLabels : this.rightMonitorLabels;
}
updateMonitorLabel() {
// Extract all labels
this.leftMonitorLabels.clear();
this.rightMonitorLabels.clear();
this.transferData.forEach(item => {
item.labels.forEach((label: string) => {
if (item.direction === 'left') {
this.leftMonitorLabels.add(label);
} else {
this.rightMonitorLabels.add(label);
}
});
});
}
handleSearch(direction: string): void {
// keep the items that are not in the current direction
let keepItems: TransferItem[] = this.filteredTransferData.filter(item => item.direction != direction);
// if the search value is empty
if (
(direction == 'left' && !this.leftSearchValue && !this.leftFilterLabels.length) ||
(direction == 'right' && !this.rightSearchValue && !this.rightFilterLabels.length)
) {
const filteredItems = this.transferData.filter(item => item.direction === direction);
this.filteredTransferData = [...keepItems, ...filteredItems];
return;
}
// Handle name search
const nameSearchResult = this.handelNameSearch(direction);
// Handle label search
const labelSearchResult = this.handelLabelSearch(direction);
// Create Map of items by key for efficient lookup
const nameSearchMap = new Map(nameSearchResult.map(item => [item.title, item]));
// Find intersection - only keep items that exist in both result sets
const filteredItems = labelSearchResult.filter(item => nameSearchMap.has(item.title));
const result = [...keepItems, ...filteredItems];
result.sort((a, b) => a.title.localeCompare(b.title));
this.filteredTransferData = result;
}
handelNameSearch(direction: string): TransferItem[] {
// handel name search
const searchValue = this.getSearchValue(direction);
// filter the items that match the search value
const filteredItems = this.transferData.filter(item => {
if (item.direction !== direction) {
// If not the current direction, skip filtering
return false;
}
return item.title.toLowerCase().includes(searchValue.toLowerCase());
});
return filteredItems;
}
handelLabelSearch(direction: string): TransferItem[] {
// handel label search
const filterLabels = this.getFilterLabels(direction);
if (filterLabels.length === 0) {
const filteredItems = this.transferData.filter(item => item.direction === direction);
return filteredItems;
}
// filter the items that match the filter labels
const filteredItems = this.transferData.filter(item => {
if (item.direction !== direction) {
// If not the current direction, skip filtering
return false;
}
const labelSet = new Set(item.labels);
return filterLabels.some(label => labelSet.has(label));
});
return filteredItems;
}
// end -- associate alert definition and monitoring model
private cascadeValuesToExpr(values: string[]): string {
@@ -1030,6 +1187,9 @@ export class AlertSettingComponent implements OnInit {
// Clean up any remaining && at start/end
.replace(/^\s*&&\s*/, '')
.replace(/\s*&&\s*$/, '')
// Remove monitor label binding expressions
.replace(/&&\s*\(?(equals\(__labels__,\s*"[^"]+"\)(\s*or\s*equals\(__labels__,\s*"[^"]+"\))*)\)?/, '')
.replace(/\(?(equals\(__labels__,\s*"[^"]+"\)(\s*or\s*equals\(__labels__,\s*"[^"]+"\))*)\)?\s*&&\s*/, '')
);
}
@@ -1061,11 +1221,12 @@ export class AlertSettingComponent implements OnInit {
public updateFinalExpr(): void {
const baseExpr = this.cascadeValuesToExpr(this.cascadeValues);
const monitorBindExpr = this.generateMonitorBindExpr();
const monitorLabelBindExpr = this.generateMonitorLabelBindExpr();
let thresholdExpr = '';
if (this.cascadeValues.length >= 2 && this.cascadeValues[1] !== 'availability') {
thresholdExpr = this.userExpr;
}
const exprList = [baseExpr, monitorBindExpr, thresholdExpr].filter(e => e);
const exprList = [baseExpr, monitorBindExpr, monitorLabelBindExpr, thresholdExpr].filter(e => e);
this.define.expr = exprList.length > 1 ? exprList.join(' && ') : exprList[0];
}
@@ -1138,6 +1299,16 @@ export class AlertSettingComponent implements OnInit {
}
}
// Parse label from expression
private parseLabelFromExpr(expr: string) {
const labelPattern = /equals\(__labels__,\s*"([^"]+)"\)/g;
let match;
this.selectedLabels.clear();
while ((match = labelPattern.exec(expr)) !== null) {
this.selectedLabels.add(match[1]);
}
}
// Generate monitor binding expression
private generateMonitorBindExpr(): string {
if (this.selectedMonitorIds.size === 0) return '';
@@ -1147,6 +1318,15 @@ export class AlertSettingComponent implements OnInit {
return this.selectedMonitorIds.size > 1 ? `(${idExprs})` : idExprs;
}
// Generate monitor label binding expression
private generateMonitorLabelBindExpr(): string {
if (this.selectedLabels.size === 0) return '';
const labelExprs = Array.from(this.selectedLabels)
.map(label => `contains(__labels__, "${label}")`)
.join(' or ');
return this.selectedLabels.size > 1 ? `(${labelExprs})` : labelExprs;
}
// Load monitor binds
showConnectModal() {
if (this.cascadeValues.length < 2) {
@@ -1156,6 +1336,7 @@ export class AlertSettingComponent implements OnInit {
// Parse monitor IDs from expr first
if (this.define.expr) {
this.parseMonitorIdsFromExpr(this.define.expr);
this.parseLabelFromExpr(this.define.expr);
}
this.monitorSvc.getMonitorsByApp(this.cascadeValues[0]).subscribe(message => {
if (message.code === 0) {
@@ -1165,8 +1346,11 @@ export class AlertSettingComponent implements OnInit {
key: item.id,
title: item.name,
description: item.host,
direction: this.selectedMonitorIds.has(item.id) ? 'right' : 'left'
direction: this.selectedMonitorIds.has(item.id) ? 'right' : 'left',
labels: Object.entries(item.labels).map(([key, value]) => `${key}:${value}`)
}));
this.updateMonitorLabel();
this.filteredTransferData = [...this.transferData];
}
});
this.isConnectModalVisible = true;
+3 -1
View File
@@ -26,6 +26,7 @@ import { NzCascaderModule } from 'ng-zorro-antd/cascader';
import { NzCollapseModule } from 'ng-zorro-antd/collapse';
import { NzDatePickerModule } from 'ng-zorro-antd/date-picker';
import { NzDividerModule } from 'ng-zorro-antd/divider';
import { NzEmptyModule } from 'ng-zorro-antd/empty';
import { NzListModule } from 'ng-zorro-antd/list';
import { NzPaginationModule } from 'ng-zorro-antd/pagination';
import { NzRadioModule } from 'ng-zorro-antd/radio';
@@ -76,7 +77,8 @@ const COMPONENTS: Array<Type<void>> = [
NzBadgeModule,
NzUploadModule,
QueryBuilderModule,
NzPaginationModule
NzPaginationModule,
NzEmptyModule
],
declarations: COMPONENTS
})
@@ -1,4 +1,4 @@
@import "~src/styles/theme";
@import '~src/styles/theme';
::ng-deep {
.monitor-select-menu-modal {
@@ -158,16 +158,16 @@
display: flex;
justify-content: space-between;
align-items: center;
button {
padding: 0;
height: auto;
i {
margin-right: 4px;
}
}
.action-button {
height: 32px;
padding: 0 8px;
@@ -197,7 +197,7 @@
color: #999;
font-size: 12px;
white-space: nowrap;
i {
margin-right: 4px;
}
@@ -209,7 +209,7 @@
.monitor-card-status {
margin-left: auto;
.ant-tag {
margin-right: 0;
}
@@ -228,12 +228,12 @@
flex-direction: column;
align-items: flex-start;
}
.time-ago {
margin-left: 0;
margin-top: 8px;
}
.label-tags {
max-width: 100%;
}
@@ -245,68 +245,68 @@
background-color: @common-background-color-dark;
border-color: #303030;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
&:hover {
border-color: @primary-color;
box-shadow: 0 1px 8px rgba(24, 144, 255, 0.3);
}
}
.monitor-card-title button {
color: @primary-color;
}
.time-ago {
color: #a0a0a0;
}
.host-item button[nzType="text"] {
.host-item button[nzType='text'] {
color: #d9d9d9;
&:hover {
color: @primary-color;
}
}
.action-button {
background-color: #303030;
border-color: #434343;
&:hover {
background-color: #1f1f1f;
border-color: @primary-color;
}
}
.label-tags .ant-tag {
background-color: #303030;
}
.export-type-card {
background-color: @common-background-color-dark;
border-color: #303030;
&:hover {
border-color: @primary-color;
box-shadow: 0 0 8px rgba(24, 144, 255, 0.3);
}
&.loading {
opacity: 0.5;
background-color: #1f1f1f;
}
}
.export-type-icon {
color: @primary-color;
background: rgba(24, 144, 255, 0.2);
}
.export-type-info {
h3 {
color: #d9d9d9;
}
p {
color: #a0a0a0;
}
+7
View File
@@ -229,6 +229,12 @@
"alert.setting.connect": "Alert Threshold Associate Monitors",
"alert.setting.connect.left": "No Associate",
"alert.setting.connect.right": "Associated",
"alert.setting.connect.instance": "Instance Associate",
"alert.setting.connect.label": "Label Associate",
"alert.setting.connect.label.input": "Input Label",
"alert.setting.connect.label.empty": "No matching monitoring objects found, please add labels",
"alert.setting.filter.name": "Input name to filter...",
"alert.setting.filter.labels": "Input labels to filter...",
"alert.setting.default": "Global Default",
"alert.setting.default.tip": "Whether this alarm threshold configuration applies to all this type of monitoring globally",
"alert.setting.delete": "Delete Threshold Rule",
@@ -303,6 +309,7 @@
"alert.setting.template.vars.instance": "Instance ID",
"alert.setting.template.vars.instance-name": "Instance Name",
"alert.setting.template.vars.instance-host": "Instance Host",
"alert.setting.template.vars.labels": "Labels",
"alert.setting.template.vars.metrics": "Metrics Name",
"alert.setting.template.vars.threshold": "Threshold Expression",
"alert.setting.template.vars.time": "Trigger Time",
+7
View File
@@ -229,6 +229,12 @@
"alert.setting.connect": "アラート閾値をモニターに関連付ける",
"alert.setting.connect.left": "関連付けなし",
"alert.setting.connect.right": "関連付け済み",
"alert.setting.connect.instance": "インスタンス関連",
"alert.setting.connect.label": "ラベル関連",
"alert.setting.connect.label.input": "ラベルを入力",
"alert.setting.connect.label.empty": "関連するモニターが見つかりません。ラベルを追加してください",
"alert.setting.filter.name": "名前を入力してフィルター...",
"alert.setting.filter.labels": "ラベルを入力してフィルター...",
"alert.setting.default": "グローバルデフォルト",
"alert.setting.default.tip": "このアラーム閾値設定がグローバルにこのタイプの監視に適用されるかどうか",
"alert.setting.delete": "閾値ルールを削除",
@@ -303,6 +309,7 @@
"alert.setting.template.vars.instance": "インスタンスID",
"alert.setting.template.vars.instance-name": "インスタンス名",
"alert.setting.template.vars.instance-host": "インスタンスホスト",
"alert.setting.template.vars.labels": "タグ",
"alert.setting.template.vars.metrics": "メトリック名",
"alert.setting.template.vars.threshold": "閾値式",
"alert.setting.template.vars.time": "トリガー時間",
File diff suppressed because it is too large Load Diff
+7
View File
@@ -229,6 +229,12 @@
"alert.setting.connect": "阈值规则关联监控",
"alert.setting.connect.left": "未关联监控",
"alert.setting.connect.right": "已关联监控",
"alert.setting.connect.instance": "实例关联",
"alert.setting.connect.label": "标签关联",
"alert.setting.connect.label.input": "输入标签",
"alert.setting.connect.label.empty": "暂无匹配的监控对象,请添加标签",
"alert.setting.filter.name": "输入名称进行过滤...",
"alert.setting.filter.labels": "输入标签进行过滤...",
"alert.setting.default": "应用全局",
"alert.setting.default.tip": "此告警阈值配置是否应用于全局所有此类型监控",
"alert.setting.delete": "删除阈值规则",
@@ -303,6 +309,7 @@
"alert.setting.template.vars.instance": "实例ID",
"alert.setting.template.vars.instance-name": "实例名称",
"alert.setting.template.vars.instance-host": "实例Host",
"alert.setting.template.vars.labels": "标签",
"alert.setting.template.vars.metrics": "指标名称",
"alert.setting.template.vars.threshold": "阈值表达式",
"alert.setting.template.vars.time": "触发时间",
+7
View File
@@ -228,6 +228,12 @@
"alert.setting.connect": "閾值規則關聯監控",
"alert.setting.connect.left": "未關聯監控",
"alert.setting.connect.right": "已關聯監控",
"alert.setting.connect.instance": "實例關聯",
"alert.setting.connect.label": "標籤關聯",
"alert.setting.connect.label.input": "輸入標籤",
"alert.setting.connect.label.empty": "暫無匹配的監控對象,請添加標籤",
"alert.setting.filter.name": "輸入名稱進行過濾...",
"alert.setting.filter.labels": "輸入標籤進行過濾...",
"alert.setting.default": "應用全域",
"alert.setting.default.tip": "此告警阈值配置是否應用于全局所有此類型監控",
"alert.setting.delete": "刪除阈值規則",
@@ -302,6 +308,7 @@
"alert.setting.template.vars.instance": "实例ID",
"alert.setting.template.vars.instance-name": "实例名称",
"alert.setting.template.vars.instance-host": "实例Host",
"alert.setting.template.vars.labels": "标签",
"alert.setting.template.vars.metrics": "指標名稱",
"alert.setting.template.vars.threshold": "閾值表達式",
"alert.setting.template.vars.time": "觸發時間",