Skip to content

Commit c01d0e2

Browse files
committed
feat:新增告警记录接口,与前端联通
1 parent 5fdcc93 commit c01d0e2

File tree

7 files changed

+345
-45
lines changed

7 files changed

+345
-45
lines changed

client/src/views/ChangeLogView.vue

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@
8484
import { ref, computed, onMounted, watch } from 'vue'
8585
import { useAppStore, type ChangeItem, type AlarmChangeItem } from '@/stores/app'
8686
import { mockApi } from '@/mock/api'
87-
import type { DeploymentChangelogResponse, DeploymentChangelogItem, AlertRuleChangelogResponse, AlertRuleChangeItem } from '@/mock/services'
87+
import { apiService } from '@/api'
88+
import type { DeploymentChangelogResponse } from '@/mock/services'
8889
import ChangeCard from '@/components/ChangeCard.vue'
8990
import AlarmChangeCard from '@/components/AlarmChangeCard.vue'
9091
import { ArrowLeft, Loading } from '@element-plus/icons-vue'
@@ -154,7 +155,7 @@ const transformDeploymentChangelogToChangeItems = (changelogData: any[]): Change
154155
}
155156
156157
// 数据转换函数:将告警规则变更记录API返回的数据转换为前端需要的格式
157-
const transformAlertRuleChangelogToAlarmChangeItems = (changelogData: AlertRuleChangeItem[]): AlarmChangeItem[] => {
158+
const transformAlertRuleChangelogToAlarmChangeItems = (changelogData: any[]): AlarmChangeItem[] => {
158159
return changelogData.map((item, index) => {
159160
// 从scope中提取服务名
160161
const serviceName = item.scope?.startsWith('service:') ? item.scope.slice('service:'.length) + '服务' : '全局服务'
@@ -200,21 +201,21 @@ const loadDeploymentChangelog = async (start?: string, limit?: number) => {
200201
}
201202
}
202203
203-
// 加载告警规则变更记录
204+
// 加载告警规则变更记录(使用真实 API)
204205
const loadAlertRuleChangelog = async (start?: string, limit?: number) => {
205206
if (alertRuleLoading.value) return // 防止重复加载
206207
207208
try {
208209
alertRuleLoading.value = true
209210
error.value = null
210211
211-
const response = await mockApi.getAlertRuleChangelog(start, limit)
212-
alertRuleChangelog.value = response
212+
const response = await apiService.getAlertRuleChangelog(start, limit ?? 10)
213+
alertRuleChangelog.value = response.data
213214
214215
// 转换数据格式
215-
alarmChangeItems.value = transformAlertRuleChangelogToAlarmChangeItems(response.items)
216+
alarmChangeItems.value = transformAlertRuleChangelogToAlarmChangeItems(response.data.items)
216217
217-
console.log('告警规则变更记录加载成功:', response)
218+
console.log('告警规则变更记录加载成功:', response.data)
218219
} catch (err) {
219220
error.value = '加载告警规则变更记录失败'
220221
console.error('加载告警规则变更记录失败:', err)

docs/alerting/api.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,49 @@ curl -X POST http://localhost:8080/v1/integrations/alertmanager/webhook \
315315
}'
316316
```
317317

318+
### 4. 获取告警规则变更记录
319+
320+
用于查询统一化告警规则的变更记录(阈值、观察窗口等),支持按时间游标分页。
321+
322+
**请求:**
323+
```http
324+
GET /v1/changelog/alertrules?start={start}&limit={limit}
325+
```
326+
327+
**查询参数:**
328+
329+
| 参数名 | 类型 | 必填 | 说明 |
330+
|--------|------|------|------|
331+
| start | string || 游标时间(ISO 8601)。第一页可不传;翻页使用上次响应的 `next` |
332+
| limit | integer || 返回数量,范围 1-100 |
333+
334+
分页说明:按 `change_time` 倒序返回,`start` 为上界(`<= start`)。响应中的 `next` 为当前页最后一条的 `editTime`
335+
336+
**响应示例:**
337+
```json
338+
{
339+
"items": [
340+
{
341+
"name": "http_request_latency_p98_seconds_P1",
342+
"editTime": "2024-01-03T03:00:00Z",
343+
"scope": "",
344+
"values": [
345+
{"name": "threshold", "old": "10", "new": "15"}
346+
],
347+
"reason": "Update"
348+
}
349+
],
350+
"next": "2024-01-03T03:00:00Z"
351+
}
352+
```
353+
354+
**状态码:**
355+
- `200 OK`: 成功
356+
- `400 Bad Request`: 参数错误
357+
- `401 Unauthorized`: 认证失败
358+
- `500 Internal Server Error`: 服务器内部错误
359+
318360
## 版本历史
319361

362+
- **v1.1** (2025-10-07): 新增 `GET /v1/changelog/alertrules`
320363
- **v1.0** (2025-09-11): 初始版本,支持基础的告警列表和详情查询

internal/alerting/api/issues_api.go

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ func RegisterIssueRoutes(router *gin.Engine, rdb *redis.Client, db *adb.Database
2828
api := &IssueAPI{R: rdb, DB: db}
2929
router.GET("/v1/issues/:issueID", api.GetIssueByID)
3030
router.GET("/v1/issues", api.ListIssues)
31+
router.GET("/v1/changelog/alertrules", api.ListAlertRuleChangeLogs)
3132
}
3233

3334
func newRedisFromEnv() *redis.Client { return nil }
@@ -257,3 +258,138 @@ func (api *IssueAPI) ListIssues(c *gin.Context) {
257258
}
258259
c.JSON(http.StatusOK, resp)
259260
}
261+
262+
// ===== Alert Rule ChangeLog =====
263+
264+
type alertRuleChangeValue struct {
265+
Name string `json:"name"`
266+
Old string `json:"old"`
267+
New string `json:"new"`
268+
}
269+
270+
type alertRuleChangeItem struct {
271+
Name string `json:"name"`
272+
EditTime string `json:"editTime"`
273+
Scope string `json:"scope"`
274+
Values []alertRuleChangeValue `json:"values"`
275+
Reason string `json:"reason"`
276+
}
277+
278+
type alertRuleChangeListResponse struct {
279+
Items []alertRuleChangeItem `json:"items"`
280+
Next string `json:"next,omitempty"`
281+
}
282+
283+
// ListAlertRuleChangeLogs implements GET /v1/changelog/alertrules?start=...&limit=...
284+
func (api *IssueAPI) ListAlertRuleChangeLogs(c *gin.Context) {
285+
if api.DB == nil {
286+
c.JSON(http.StatusInternalServerError, map[string]any{"error": map[string]any{"code": "INTERNAL_ERROR", "message": "database not configured"}})
287+
return
288+
}
289+
290+
start := strings.TrimSpace(c.Query("start"))
291+
limitStr := strings.TrimSpace(c.Query("limit"))
292+
if limitStr == "" {
293+
c.JSON(http.StatusBadRequest, map[string]any{"error": map[string]any{"code": "INVALID_PARAMETER", "message": "limit is required"}})
294+
return
295+
}
296+
limit, err := strconv.Atoi(limitStr)
297+
if err != nil || limit < 1 || limit > 100 {
298+
c.JSON(http.StatusBadRequest, map[string]any{"error": map[string]any{"code": "INVALID_PARAMETER", "message": "limit must be 1-100"}})
299+
return
300+
}
301+
302+
var (
303+
q string
304+
args []any
305+
)
306+
if start == "" {
307+
q = `
308+
SELECT alert_name, change_time, labels, old_threshold, new_threshold, change_type
309+
FROM alert_meta_change_logs
310+
WHERE change_type = 'Update'
311+
ORDER BY change_time DESC
312+
LIMIT $1`
313+
args = append(args, limit)
314+
} else {
315+
if _, err := time.Parse(time.RFC3339, start); err != nil {
316+
if _, err2 := time.Parse(time.RFC3339Nano, start); err2 != nil {
317+
c.JSON(http.StatusBadRequest, map[string]any{"error": map[string]any{"code": "INVALID_PARAMETER", "message": "start must be ISO 8601 time"}})
318+
return
319+
}
320+
}
321+
q = `
322+
SELECT alert_name, change_time, labels, old_threshold, new_threshold, change_type
323+
FROM alert_meta_change_logs
324+
WHERE change_time <= $1 AND change_type = 'Update'
325+
ORDER BY change_time DESC
326+
LIMIT $2`
327+
args = append(args, start, limit)
328+
}
329+
330+
rows, err := api.DB.QueryContext(c.Request.Context(), q, args...)
331+
if err != nil {
332+
c.JSON(http.StatusInternalServerError, map[string]any{"error": map[string]any{"code": "INTERNAL_ERROR", "message": err.Error()}})
333+
return
334+
}
335+
defer rows.Close()
336+
337+
items := make([]alertRuleChangeItem, 0, limit)
338+
var lastTime string
339+
for rows.Next() {
340+
var (
341+
name string
342+
changeTime time.Time
343+
labelsRaw string
344+
oldTh *float64
345+
newTh *float64
346+
changeType string
347+
)
348+
if err := rows.Scan(&name, &changeTime, &labelsRaw, &oldTh, &newTh, &changeType); err != nil {
349+
continue
350+
}
351+
scope := ""
352+
var lm map[string]any
353+
if err := json.Unmarshal([]byte(labelsRaw), &lm); err == nil {
354+
if svc, ok := lm["service"].(string); ok && svc != "" {
355+
scope = "service:" + svc
356+
if ver, ok := lm["service_version"].(string); ok && ver != "" {
357+
scope = scope + "v" + ver
358+
}
359+
}
360+
}
361+
362+
values := make([]alertRuleChangeValue, 0, 2)
363+
if oldTh != nil || newTh != nil {
364+
values = append(values, alertRuleChangeValue{
365+
Name: "threshold",
366+
Old: floatToString(oldTh),
367+
New: floatToString(newTh),
368+
})
369+
}
370+
371+
item := alertRuleChangeItem{
372+
Name: name,
373+
EditTime: changeTime.UTC().Format(time.RFC3339),
374+
Scope: scope,
375+
Values: values,
376+
Reason: "检测到异常且未发生告警,降低阈值以尽早发现问题",
377+
}
378+
items = append(items, item)
379+
lastTime = item.EditTime
380+
}
381+
382+
resp := alertRuleChangeListResponse{Items: items}
383+
if lastTime != "" {
384+
resp.Next = lastTime
385+
}
386+
c.JSON(http.StatusOK, resp)
387+
}
388+
389+
func floatToString(p *float64) string {
390+
if p == nil {
391+
return ""
392+
}
393+
s := strconv.FormatFloat(*p, 'f', -1, 64)
394+
return s
395+
}

internal/alerting/service/healthcheck/bootstrap.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,14 @@ func BootstrapRulesFromConfigWithApp(ctx context.Context, db *adb.Database, c *c
8383
for _, m := range r.Metas {
8484
labelsJSON, _ := json.Marshal(m.Labels)
8585
metaUpdates = append(metaUpdates, ruleMetaUpdate{Labels: string(labelsJSON), Threshold: m.Threshold})
86+
log.Debug().Str("rule", r.Name).
87+
Str("bootstrap_labels", string(labelsJSON)).
88+
Str("bootstrap_labels_ckey", canonicalKeyFromLabelsJSON(string(labelsJSON))).
89+
Float64("bootstrap_threshold", m.Threshold).
90+
Msg("bootstrap meta item")
8691
}
8792
if len(metaUpdates) > 0 {
88-
if err := putRuleMetasBootstrap(ctx, client, base, r.Name, metaUpdates); err != nil {
93+
if err := putRuleMetas(ctx, client, base, r.Name, metaUpdates); err != nil {
8994
log.Error().Err(err).Str("rule", r.Name).Msg("external PUT rule metas failed")
9095
continue
9196
}
@@ -147,13 +152,16 @@ func parseWatchTimeToSeconds(watchTime string) (int, error) {
147152
return int(duration.Seconds()), nil
148153
}
149154

150-
// putRuleMetasBootstrap calls PUT /v1/alert-rules-meta/{rule_name} with the correct format
151-
func putRuleMetasBootstrap(ctx context.Context, client *http.Client, base, ruleName string, metas []ruleMetaUpdate) error {
155+
// putRuleMetas calls PUT /v1/alert-rules-meta/{rule_name} with the correct format
156+
func putRuleMetas(ctx context.Context, client *http.Client, base, ruleName string, metas []ruleMetaUpdate) error {
152157
endpoint := base + "/v1/alert-rules-meta/" + url.PathEscape(ruleName)
158+
log.Debug().Str("endpoint", endpoint).Str("rule_name", ruleName).Int("meta_count", len(metas)).Msg("prepared bootstrap ruleset meta PUT endpoint")
153159
body := map[string]interface{}{
154160
"metas": metas,
155161
}
162+
log.Debug().Any("body", body).Msg("bootstrap ruleset meta PUT body")
156163
bs, _ := json.Marshal(body)
164+
log.Debug().Str("bs", string(bs)).Msg("bootstrap ruleset meta PUT bs")
157165
req, err := http.NewRequestWithContext(ctx, http.MethodPut, endpoint, io.NopCloser(strings.NewReader(string(bs))))
158166
if err != nil {
159167
return err

0 commit comments

Comments
 (0)