Compare commits

...

5 Commits

Author SHA1 Message Date
Yening Qin
aff584657c Merge branch 'main' into event-trigger-value-add-unit 2024-11-15 17:09:27 +08:00
ning
820286988f code refactor 2024-11-15 15:31:51 +08:00
ning
07a94e2eef code refactor 2024-11-15 15:12:36 +08:00
ning
8f2c5dea69 code refactor 2024-11-15 09:52:37 +08:00
Xu Bin
db58975575 unit for value (#2274) 2024-11-14 23:50:41 +08:00
7 changed files with 693 additions and 9 deletions

View File

@@ -6,19 +6,21 @@ import (
"strings"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/unit"
"github.com/prometheus/common/model"
)
type AnomalyPoint struct {
Key string `json:"key"`
Labels model.Metric `json:"labels"`
Timestamp int64 `json:"timestamp"`
Value float64 `json:"value"`
Severity int `json:"severity"`
Triggered bool `json:"triggered"`
Query string `json:"query"`
Values string `json:"values"`
RecoverConfig models.RecoverConfig `json:"recover_config"`
Key string `json:"key"`
Labels model.Metric `json:"labels"`
Timestamp int64 `json:"timestamp"`
Value float64 `json:"value"`
Severity int `json:"severity"`
Triggered bool `json:"triggered"`
Query string `json:"query"`
Values string `json:"values"`
ValuesUnit map[string]unit.FormattedValue `json:"values_unit"`
RecoverConfig models.RecoverConfig `json:"recover_config"`
}
func NewAnomalyPoint(key string, labels map[string]string, ts int64, value float64, severity int) AnomalyPoint {

View File

@@ -18,6 +18,7 @@ import (
"github.com/ccfos/nightingale/v6/pkg/hash"
"github.com/ccfos/nightingale/v6/pkg/parser"
promsdk "github.com/ccfos/nightingale/v6/pkg/prom"
"github.com/ccfos/nightingale/v6/pkg/unit"
"github.com/ccfos/nightingale/v6/prom"
"github.com/ccfos/nightingale/v6/tdengine"
@@ -247,7 +248,11 @@ func (arw *AlertRuleWorker) GetPromAnomalyPoint(ruleConfig string) ([]common.Ano
for i := 0; i < len(points); i++ {
points[i].Severity = query.Severity
points[i].Query = promql
points[i].ValuesUnit = map[string]unit.FormattedValue{
"v": unit.ValueFormatter(query.Unit, 2, points[i].Value),
}
}
lst = append(lst, points...)
}
return lst, nil
@@ -471,11 +476,22 @@ func GetAnomalyPoint(ruleId int64, ruleQuery models.RuleQuery, seriesTagIndexes
return points, recoverPoints
}
unitMap := make(map[string]string)
for _, query := range ruleQuery.Queries {
ref, unit, err := GetQueryRefAndUnit(query)
if err != nil {
continue
}
unitMap[ref] = unit
}
for _, trigger := range ruleQuery.Triggers {
// seriesTagIndex 的 key 仅做分组使用value 为每组 series 的 hash
seriesTagIndex := ProcessJoins(ruleId, trigger, seriesTagIndexes, seriesStore)
for _, seriesHash := range seriesTagIndex {
valuesUnitMap := make(map[string]unit.FormattedValue)
sort.Slice(seriesHash, func(i, j int) bool {
return seriesHash[i] < seriesHash[j]
})
@@ -501,6 +517,10 @@ func GetAnomalyPoint(ruleId int64, ruleQuery models.RuleQuery, seriesTagIndexes
continue
}
if u, exists := unitMap[series.Ref]; exists {
valuesUnitMap[series.Ref] = unit.ValueFormatter(u, 2, v)
}
m["$"+series.Ref] = v
m["$"+series.Ref+"."+series.MetricName()] = v
ts = int64(t)
@@ -529,6 +549,7 @@ func GetAnomalyPoint(ruleId int64, ruleQuery models.RuleQuery, seriesTagIndexes
Triggered: isTriggered,
Query: fmt.Sprintf("query:%+v trigger:%+v", ruleQuery.Queries, trigger),
RecoverConfig: trigger.RecoverConfig,
ValuesUnit: valuesUnitMap,
}
if sample.Query != "" {
@@ -815,3 +836,18 @@ func GetQueryRef(query interface{}) (string, error) {
return refField.String(), nil
}
func GetQueryRefAndUnit(query interface{}) (string, string, error) {
type Query struct {
Ref string `json:"ref"`
Unit string `json:"unit"`
}
queryMap := Query{}
queryBytes, err := json.Marshal(query)
if err != nil {
return "", "", err
}
json.Unmarshal(queryBytes, &queryMap)
return queryMap.Ref, queryMap.Unit, nil
}

View File

@@ -204,6 +204,7 @@ func (p *Processor) BuildEvent(anomalyPoint common.AnomalyPoint, from string, no
event.TargetNote = p.targetNote
event.TriggerValue = anomalyPoint.ReadableValue()
event.TriggerValues = anomalyPoint.Values
event.TriggerValuesJson = models.EventTriggerValues{ValuesWithUnit: anomalyPoint.ValuesUnit}
event.TagsJSON = p.tagsArr
event.Tags = strings.Join(p.tagsArr, ",,")
event.IsRecovered = false

View File

@@ -12,6 +12,7 @@ import (
"github.com/ccfos/nightingale/v6/pkg/ctx"
"github.com/ccfos/nightingale/v6/pkg/poster"
"github.com/ccfos/nightingale/v6/pkg/tplx"
"github.com/ccfos/nightingale/v6/pkg/unit"
"github.com/toolkits/pkg/logger"
)
@@ -49,6 +50,7 @@ type AlertCurEvent struct {
TriggerTime int64 `json:"trigger_time"`
TriggerValue string `json:"trigger_value"`
TriggerValues string `json:"trigger_values" gorm:"-"`
TriggerValuesJson EventTriggerValues `json:"trigger_values_json" gorm:"-"`
Tags string `json:"-"` // for db
TagsJSON []string `json:"tags" gorm:"-"` // for fe
TagsMap map[string]string `json:"tags_map" gorm:"-"` // for internal usage
@@ -73,6 +75,10 @@ type AlertCurEvent struct {
ExtraInfoMap []map[string]string `json:"extra_info_map" gorm:"-"`
}
type EventTriggerValues struct {
ValuesWithUnit map[string]unit.FormattedValue `json:"values_with_unit"`
}
func (e *AlertCurEvent) TableName() string {
return "alert_cur_event"
}

View File

@@ -155,6 +155,7 @@ type PromQuery struct {
PromQl string `json:"prom_ql"`
Severity int `json:"severity"`
RecoverConfig RecoverConfig `json:"recover_config"`
Unit string `json:"unit"`
}
type HostTrigger struct {

320
pkg/unit/unit_convert.go Normal file
View File

@@ -0,0 +1,320 @@
package unit
import (
"fmt"
"math"
"strings"
"time"
)
// FormattedValue 格式化后的值的结构
type FormattedValue struct {
Value float64 `json:"value"`
Unit string `json:"unit"`
Text string `json:"text"`
Stat float64 `json:"stat"`
}
// FormatOptions 格式化选项
type FormatOptions struct {
Type string // "si" 或 "iec"
Base string // "bits" 或 "bytes"
Decimals int // 小数位数
Postfix string // 后缀
}
// 时间相关常量
const (
NanosecondVal = 0.000000001
MicrosecondVal = 0.000001
MillisecondVal = 0.001
SecondVal = 1
MinuteVal = 60
HourVal = 3600
DayVal = 86400
WeekVal = 86400 * 7
YearVal = 86400 * 365
)
var (
valueMap = []struct {
Exp int
Si string
Iec string
IecExp int
}{
{0, "", "", 1},
{3, "k", "Ki", 10},
{6, "M", "Mi", 20},
{9, "G", "Gi", 30},
{12, "T", "Ti", 40},
{15, "P", "Pi", 50},
{18, "E", "Ei", 60},
{21, "Z", "Zi", 70},
{24, "Y", "Yi", 80},
}
baseUtilMap = map[string]string{
"bits": "b",
"bytes": "B",
}
)
// ValueFormatter 格式化入口函数
func ValueFormatter(unit string, decimals int, value float64) FormattedValue {
if math.IsNaN(value) {
return FormattedValue{
Value: 0,
Unit: "",
Text: "NaN",
Stat: 0,
}
}
// 处理时间单位
switch unit {
case "none":
return formatNone(value, decimals)
case "ns", "nanoseconds":
return formatDuration(value, "ns", decimals)
case "µs", "microseconds":
return formatDuration(value, "µs", decimals)
case "ms", "milliseconds":
return formatDuration(value, "ms", decimals)
case "s", "seconds":
return formatDuration(value, "s", decimals)
case "min", "h", "d", "w":
return formatDuration(value, unit, decimals)
case "percent":
return formatPercent(value, decimals, false)
case "percentUnit":
return formatPercent(value, decimals, true)
case "bytesIEC", "bytes(IEC)", "bitsIEC", "bits(IEC)":
base := unit
base = strings.TrimSuffix(base, "(IEC)")
base = strings.TrimSuffix(base, "IEC")
base = strings.TrimSuffix(base, "s")
opts := FormatOptions{
Type: "iec",
Base: base,
Decimals: decimals,
}
return formatBytes(value, opts)
case "bytesSI", "bytes(SI)", "bitsSI", "bits(SI)", "default", "sishort":
base := unit
base = strings.TrimSuffix(base, "(SI)")
base = strings.TrimSuffix(base, "SI")
base = strings.TrimSuffix(base, "s")
opts := FormatOptions{
Type: "si",
Base: base,
Decimals: decimals,
}
return formatBytes(value, opts)
case "bytesSecIEC":
opts := FormatOptions{
Type: "iec",
Base: "bytes",
Decimals: decimals,
Postfix: "/s",
}
return formatBytes(value, opts)
case "bitsSecIEC":
opts := FormatOptions{
Type: "iec",
Base: "bits",
Decimals: decimals,
Postfix: "/s",
}
return formatBytes(value, opts)
case "bytesSecSI":
opts := FormatOptions{
Type: "si",
Base: "bytes",
Decimals: decimals,
Postfix: "/s",
}
return formatBytes(value, opts)
case "bitsSecSI":
opts := FormatOptions{
Type: "si",
Base: "bits",
Decimals: decimals,
Postfix: "/s",
}
return formatBytes(value, opts)
case "datetimeSeconds", "datetimeMilliseconds":
return formatDateTime(unit, value)
default:
return formatNone(value, decimals)
}
}
// formatDuration 处理时间单位的转换
func formatDuration(originValue float64, unit string, decimals int) FormattedValue {
var converted float64
var targetUnit string
value := originValue
// 标准化到秒
switch unit {
case "ns":
value *= NanosecondVal
case "µs":
value *= MicrosecondVal
case "ms":
value *= MillisecondVal
case "min":
value *= MinuteVal
case "h":
value *= HourVal
case "d":
value *= DayVal
case "w":
value *= WeekVal
}
// 选择合适的单位
switch {
case value >= YearVal:
converted = value / YearVal
targetUnit = "y"
case value >= WeekVal:
converted = value / WeekVal
targetUnit = "w"
case value >= DayVal:
converted = value / DayVal
targetUnit = "d"
case value >= HourVal:
converted = value / HourVal
targetUnit = "h"
case value >= MinuteVal:
converted = value / MinuteVal
targetUnit = "min"
case value >= SecondVal:
converted = value
targetUnit = "s"
case value >= MillisecondVal:
converted = value / MillisecondVal
targetUnit = "ms"
case value >= MicrosecondVal:
converted = value / MicrosecondVal
targetUnit = "µs"
default:
converted = value / NanosecondVal
targetUnit = "ns"
}
return FormattedValue{
Value: roundFloat(converted, decimals),
Unit: targetUnit,
Text: fmt.Sprintf("%.*f %s", decimals, converted, targetUnit),
Stat: originValue,
}
}
// formatBytes 处理字节相关的转换
func formatBytes(value float64, opts FormatOptions) FormattedValue {
if value == 0 {
baseUtil := baseUtilMap[opts.Base]
return FormattedValue{
Value: 0,
Unit: baseUtil + opts.Postfix,
Text: fmt.Sprintf("0%s%s", baseUtil, opts.Postfix),
Stat: 0,
}
}
baseUtil := baseUtilMap[opts.Base]
threshold := 1000.0
if opts.Type == "iec" {
threshold = 1024.0
}
if math.Abs(value) < threshold {
return FormattedValue{
Value: roundFloat(value, opts.Decimals),
Unit: baseUtil + opts.Postfix,
Text: fmt.Sprintf("%.*f%s%s", opts.Decimals, value, baseUtil, opts.Postfix),
Stat: value,
}
}
// 计算指数
exp := int(math.Floor(math.Log10(math.Abs(value))/3.0)) * 3
if exp > 24 {
exp = 24
}
var unit string
var divider float64
// 查找对应的单位
for _, v := range valueMap {
if v.Exp == exp {
if opts.Type == "iec" {
unit = v.Iec
divider = math.Pow(2, float64(v.IecExp))
} else {
unit = v.Si
divider = math.Pow(10, float64(v.Exp))
}
break
}
}
newValue := value / divider
return FormattedValue{
Value: roundFloat(newValue, opts.Decimals),
Unit: unit + baseUtil + opts.Postfix,
Text: fmt.Sprintf("%.*f%s%s%s", opts.Decimals, newValue, unit, baseUtil, opts.Postfix),
Stat: value,
}
}
// formatPercent 处理百分比格式化
func formatPercent(value float64, decimals int, isUnit bool) FormattedValue {
if isUnit {
value = value * 100
}
return FormattedValue{
Value: roundFloat(value, decimals),
Unit: "%",
Text: fmt.Sprintf("%.*f%%", decimals, value),
Stat: value,
}
}
// formatNone 处理无单位格式化
func formatNone(value float64, decimals int) FormattedValue {
return FormattedValue{
Value: value,
Unit: "",
Text: fmt.Sprintf("%.*f", decimals, value),
Stat: value,
}
}
// formatDateTime 处理时间戳格式化
func formatDateTime(uint string, value float64) FormattedValue {
var t time.Time
switch uint {
case "datetimeSeconds":
t = time.Unix(int64(value), 0)
case "datetimeMilliseconds":
t = time.Unix(0, int64(value)*int64(time.Millisecond))
}
text := t.Format("2006-01-02 15:04:05")
return FormattedValue{
Value: value,
Unit: "",
Text: text,
Stat: value,
}
}
// roundFloat 四舍五入到指定小数位
func roundFloat(val float64, precision int) float64 {
ratio := math.Pow(10, float64(precision))
return math.Round(val*ratio) / ratio
}

View File

@@ -0,0 +1,318 @@
package unit
import (
"math"
"testing"
)
func TestValueFormatter(t *testing.T) {
tests := []struct {
name string
unit string
decimals int
value float64
want FormattedValue
}{
// 字节测试
{
name: "IEC字节测试",
unit: "bytes(IEC)",
decimals: 2,
value: 1024 * 1024,
want: FormattedValue{Value: 1, Unit: "Mi", Text: "1.00Mi", Stat: 1024 * 1024},
},
{
name: "SI字节测试",
unit: "bytes(SI)",
decimals: 2,
value: 1000 * 1000,
want: FormattedValue{Value: 1, Unit: "M", Text: "1.00M", Stat: 1000 * 1000},
},
// 时间单位测试
{
name: "毫秒转秒",
unit: "ms",
decimals: 2,
value: 1500,
want: FormattedValue{
Value: 1.50,
Unit: "s",
Text: "1.50 s",
Stat: 1500,
},
},
{
name: "秒转分钟",
unit: "s",
decimals: 1,
value: 150,
want: FormattedValue{
Value: 2.5,
Unit: "min",
Text: "2.5 min",
Stat: 150,
},
},
// 百分比测试
{
name: "百分比",
unit: "percent",
decimals: 2,
value: 0.9555,
want: FormattedValue{
Value: 0.96,
Unit: "%",
Text: "0.96%",
Stat: 0.9555,
},
},
{
name: "百分比单位",
unit: "percentUnit",
decimals: 1,
value: 0.95,
want: FormattedValue{
Value: 95.0,
Unit: "%",
Text: "95.0%",
Stat: 95.0,
},
},
// SI格式测试
{
name: "SI格式",
unit: "sishort",
decimals: 2,
value: 1500,
want: FormattedValue{
Value: 1.50,
Unit: "k",
Text: "1.50k",
Stat: 1500,
},
},
// 时间戳测试
{
name: "时间戳 s",
unit: "datetimeSeconds",
decimals: 0,
value: 1683518400,
want: FormattedValue{
Value: 1683518400,
Unit: "",
Text: "2023-05-08 12:00:00",
Stat: 1683518400,
},
},
{
name: "时间戳 ms",
unit: "datetimeMilliseconds",
decimals: 0,
value: 1683518400000,
want: FormattedValue{
Value: 1683518400000,
Unit: "",
Text: "2023-05-08 12:00:00",
Stat: 1683518400000,
},
},
// 补充时间单位测试
{
name: "纳秒测试",
unit: "ns",
decimals: 2,
value: 1500,
want: FormattedValue{
Value: 1.50,
Unit: "µs",
Text: "1.50 µs",
Stat: 1500,
},
},
{
name: "微秒测试",
unit: "µs",
decimals: 2,
value: 1500,
want: FormattedValue{
Value: 1.50,
Unit: "ms",
Text: "1.50 ms",
Stat: 1500,
},
},
{
name: "小时测试",
unit: "h",
decimals: 1,
value: 2.5,
want: FormattedValue{
Value: 2.5,
Unit: "h",
Text: "2.5 h",
Stat: 2.5,
},
},
{
name: "天数测试",
unit: "d",
decimals: 1,
value: 1.5,
want: FormattedValue{
Value: 1.5,
Unit: "d",
Text: "1.5 d",
Stat: 1.5,
},
},
{
name: "周数测试",
unit: "w",
decimals: 1,
value: 1.5,
want: FormattedValue{
Value: 1.5,
Unit: "w",
Text: "1.5 w",
Stat: 1.5,
},
},
// 补充字节速率测试
{
name: "IEC字节每秒",
unit: "bytesSecIEC",
decimals: 2,
value: 1024 * 1024,
want: FormattedValue{
Value: 1,
Unit: "MiB/s",
Text: "1.00MiB/s",
Stat: 1024 * 1024,
},
},
{
name: "IEC比特每秒",
unit: "bitsSecIEC",
decimals: 2,
value: 1024 * 1024,
want: FormattedValue{
Value: 1,
Unit: "Mib/s",
Text: "1.00Mib/s",
Stat: 1024 * 1024,
},
},
{
name: "SI字节每秒",
unit: "bytesSecSI",
decimals: 2,
value: 1000 * 1000,
want: FormattedValue{
Value: 1,
Unit: "MB/s",
Text: "1.00MB/s",
Stat: 1000 * 1000,
},
},
{
name: "SI比特每秒",
unit: "bitsSecSI",
decimals: 2,
value: 1000 * 1000,
want: FormattedValue{
Value: 1,
Unit: "Mb/s",
Text: "1.00Mb/s",
Stat: 1000 * 1000,
},
},
// none 类型测试
{
name: "无单位测试",
unit: "none",
decimals: 2,
value: 1234.5678,
want: FormattedValue{
Value: 1234.5678,
Unit: "",
Text: "1234.57",
Stat: 1234.5678,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ValueFormatter(tt.unit, tt.decimals, tt.value)
if !compareFormattedValues(got, tt.want) {
t.Errorf("ValueFormatter() = %v, want %v", got, tt.want)
}
})
}
}
func TestEdgeCases(t *testing.T) {
tests := []struct {
name string
unit string
decimals int
value float64
wantNil bool
}{
{
name: "NaN值",
unit: "bytes",
decimals: 2,
value: math.NaN(),
wantNil: false,
},
{
name: "零值",
unit: "bytes",
decimals: 2,
value: 0,
wantNil: false,
},
{
name: "极小值",
unit: "bytes",
decimals: 2,
value: 0.0000001,
wantNil: false,
},
{
name: "极大值",
unit: "bytes",
decimals: 2,
value: 1e30,
wantNil: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ValueFormatter(tt.unit, tt.decimals, tt.value)
if (got == FormattedValue{}) == !tt.wantNil {
t.Errorf("ValueFormatter() got = %v, wantNil = %v", got, tt.wantNil)
}
})
}
}
// compareFormattedValues 比较两个FormattedValue是否相等
func compareFormattedValues(a, b FormattedValue) bool {
const epsilon = 0.0001
if math.Abs(a.Value-b.Value) > epsilon {
return false
}
if math.Abs(a.Stat-b.Stat) > epsilon {
return false
}
if a.Unit != b.Unit {
return false
}
if a.Text != b.Text {
return false
}
return true
}