github.com/alibaba/ilogtail/pkg@v0.0.0-20250526110833-c53b480d046c/protocol/encoder/prometheus/encoder_prometheus_test.go (about) 1 // Copyright 2024 iLogtail Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package prometheus 16 17 import ( 18 "errors" 19 "strconv" 20 "testing" 21 22 "github.com/VictoriaMetrics/VictoriaMetrics/lib/prompb" 23 pb "github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal" 24 "github.com/prometheus/common/model" 25 "github.com/stretchr/testify/assert" 26 27 "github.com/alibaba/ilogtail/pkg/models" 28 ) 29 30 // 场景:性能测试,确定性能基线(UT粒度) 31 // 因子:所有 Event type 均为 models.EventTypeMetric 32 // 因子:所有 PipelineEvent interface 的实现均为 *models.Metric 33 // 预期:EncodeV2 和 EncodeBatchV2 性能相当(实现上 EncodeBatchV2 会循环调用 EncodeV2) 34 // Benchmark结果(每次在具体数值上可能会有差异,但数量级相同): 35 // goos: darwin 36 // goarch: arm64 37 // pkg: github.com/alibaba/ilogtail/pkg/protocol/encoder/prometheus 38 // BenchmarkV2Encode 39 // BenchmarkV2Encode/EncodeV2 40 // BenchmarkV2Encode/EncodeV2-12 685 1655657 ns/op 41 // BenchmarkV2Encode/BatchEncodeV2 42 // BenchmarkV2Encode/BatchEncodeV2-12 716 1639491 ns/op 43 // PASS 44 func BenchmarkV2Encode(b *testing.B) { 45 // given 46 p := NewPromEncoder(19) 47 groupEventsSlice := genNormalPipelineGroupEventsSlice(100) 48 want := append([]*models.PipelineGroupEvents(nil), groupEventsSlice...) 49 50 b.Run("EncodeV2", func(b *testing.B) { 51 b.ResetTimer() 52 53 for i := 0; i < b.N; i++ { 54 for _, groupEvents := range groupEventsSlice { 55 p.EncodeV2(groupEvents) 56 } 57 } 58 }) 59 assert.Equal(b, want, groupEventsSlice) 60 61 b.Run("BatchEncodeV2", func(b *testing.B) { 62 b.ResetTimer() 63 64 for i := 0; i < b.N; i++ { 65 p.EncodeBatchV2(groupEventsSlice) 66 } 67 }) 68 assert.Equal(b, want, groupEventsSlice) 69 } 70 71 // 场景:V2 Encode接口功能测试 72 // 说明:EncodeBatchV2 内部会调用 EncodeV2,所以也同时测试了 EncodeV2 的正常逻辑的功能 73 // 因子:所有 Event type 均为 models.EventTypeMetric 74 // 因子:所有 PipelineEvent interface 的实现均为「正常的*models.Metric」(而不是new(models.Metric)), 75 // 具体区别在于正常的*models.Metric,其中的Tags、Value等都不是nil(如果为nil,会触发序列化的异常逻辑) 76 // 预期:V2 Encode逻辑正常(正常流程都能正确处理),返回的error类型为nil,[][]byte不为nil 77 func TestV2Encode_ShouldReturnNoError_GivenNormalDataOfPipelineGroupEvents(t *testing.T) { 78 // given 79 groupEventsSlice1 := genNormalPipelineGroupEventsSlice(100) 80 groupEventsSlice2 := genNormalPipelineGroupEventsSlice(100) 81 p := NewPromEncoder(19) 82 83 // when 84 // then 85 data1, err1 := p.EncodeBatchV2(groupEventsSlice1) 86 assert.NoError(t, err1) 87 data2, err2 := p.EncodeBatchV2(groupEventsSlice2) 88 assert.NoError(t, err2) 89 90 assert.Equal(t, len(data2), len(data1)) 91 } 92 93 // 场景:V2 Encode接口功能测试(异常数据,非全nil或0值) 94 // 说明:尽管 EncodeBatchV2 内部会调用 EncodeV2,但异常情况可能是 EncodeBatchV2 侧的防御, 95 // 所以还需要测试异常情况下 EncodeV2 的功能 96 // 因子:并非所有 Event type 均为 models.EventTypeMetric(e.g. 还可能是 models.EventTypeLogging 等) 97 // 因子:PipelineEvent interface 的实现,部分是「正常的*models.Metric」,部分为 nil,部分为new(models.Metric), 98 // 部分为其它(e.g. *models.Log 等) 99 // 预期:Encode逻辑正常(异常流程也能正确处理),返回的error类型不为nil,[][]byte为nil 100 func TestV2Encode_ShouldReturnError_GivenAbNormalDataOfPipelineGroupEvents(t *testing.T) { 101 // given 102 groupEventsSlice1 := genPipelineGroupEventsSliceIncludingAbnormalData(100) 103 groupEventsSlice2 := genPipelineGroupEventsSliceIncludingAbnormalData(100) 104 assert.Equal(t, len(groupEventsSlice1), len(groupEventsSlice2)) 105 p := NewPromEncoder(19) 106 107 // when 108 // then 109 t.Run("Test EncodeV2 with abnormal data input", func(t *testing.T) { 110 for i, groupEvents := range groupEventsSlice1 { 111 data1, err1 := p.EncodeV2(groupEvents) 112 data2, err2 := p.EncodeV2(groupEventsSlice2[i]) 113 if err1 != nil { 114 assert.Error(t, err2) 115 assert.Equal(t, err1, err2) 116 } else { 117 assert.NoError(t, err2) 118 assert.Equal(t, len(data2), len(data1)) 119 } 120 } 121 }) 122 123 t.Run("Test EncodeBatchV2 with abnormal data input", func(t *testing.T) { 124 data1, err1 := p.EncodeBatchV2(groupEventsSlice1) 125 assert.NoError(t, err1) 126 data2, err2 := p.EncodeBatchV2(groupEventsSlice2) 127 assert.NoError(t, err2) 128 129 assert.Equal(t, len(data2), len(data1)) 130 }) 131 } 132 133 // 场景:V2 Encode接口功能测试(异常数据,全nil或0值) 134 // 说明:尽管 EncodeBatchV2 内部会调用 EncodeV2,但异常情况可能是 EncodeBatchV2 侧的防御, 135 // 所以还需要测试异常情况下 EncodeV2 的功能 136 // 因子:所有 *models.PipelineGroupEvents 及 []*models.PipelineGroupEvents 底层为 nil 或者 长度为0的切片 137 // 预期:Encode逻辑正常(异常流程也能正确处理),返回的error类型不为nil,[][]byte为nil 138 func TestV2Encode_ShouldReturnError_GivenNilOrZeroDataOfPipelineGroupEvents(t *testing.T) { 139 // given 140 p := NewPromEncoder(19) 141 nilOrZeroGroupEventsSlices := []*models.PipelineGroupEvents{ 142 nil, 143 {}, // same as {Events: nil}, 144 {Events: make([]models.PipelineEvent, 0)}, 145 } 146 nilOrZeroGroupEventsSlicesEx := [][]*models.PipelineGroupEvents{ 147 nil, 148 {}, // same as {nil} 149 {{Events: nil}}, 150 nilOrZeroGroupEventsSlices, 151 } 152 153 // when 154 // then 155 t.Run("Test EncodeV2 with nil or zero data input", func(t *testing.T) { 156 for _, input := range nilOrZeroGroupEventsSlices { 157 data, err := p.EncodeV2(input) 158 assert.Error(t, err) 159 assert.Nil(t, data) 160 } 161 }) 162 163 t.Run("Test EncodeBatchV2 with nil or zero data input", func(t *testing.T) { 164 for _, input := range nilOrZeroGroupEventsSlicesEx { 165 data, err := p.EncodeBatchV2(input) 166 assert.Error(t, err) 167 assert.Nil(t, data) 168 } 169 }) 170 } 171 172 // 场景:V2 Encode接口功能测试 173 // 说明:EncoderBatchV2 内部会调用 EncoderV2,所以也同时测试了 EncoderV2 的功能 174 // 因子:所有 Event type 均为 models.EventTypeMetric 175 // 因子:所有 PipelineEvent interface 的实现均为 *models.Metric 176 // 因子:每个 metric event 生成的 *models.Metric.Tags 中的 tag 仅一个 177 // (确保 Encode 时 range map不用考虑 range go原生map每次顺序随机,从而导致2次Encode相同数据后得到的结果不同) 178 // PS:如果不这么做,就要对map做转化,先变成 range 保序的 slice,再 Encode; 179 // 但测试的功能点是Encode,所以采用上述方法绕过range go原生map每次顺序随机的特点,完成功能测试 180 // 预期:Encode逻辑正常(正常流程都能正确处理),返回的error类型为nil,[][]byte不为nil,且两次Encode后返回的数据相同 181 func TestEncoderBatchV2_ShouldReturnNoErrorAndEqualData_GivenNormalDataOfDataOfPipelineGroupEventsWithSingleTag(t *testing.T) { 182 // given 183 groupEventsSlice1 := genPipelineGroupEventsSliceSingleTag(100) 184 groupEventsSlice2 := genPipelineGroupEventsSliceSingleTag(100) 185 p := NewPromEncoder(19) 186 187 // when 188 // then 189 data1, err1 := p.EncodeBatchV2(groupEventsSlice1) 190 assert.NoError(t, err1) 191 data2, err2 := p.EncodeBatchV2(groupEventsSlice2) 192 assert.NoError(t, err2) 193 194 assert.Equal(t, data2, data1) 195 } 196 197 // 场景:V2 Encode接口功能测试 198 // 说明:EncoderBatchV2 内部会调用 EncoderV2,所以也同时测试了 EncoderV2 的功能 199 // 因子:所有 Event type 均为 models.EventTypeMetric 200 // 因子:所有 PipelineEvent interface 的实现均为 *models.Metric 201 // 因子:每个 metric event 生成的 *models.Metric.Tags 中的 tag 仅一个 202 // (确保 Encode 时 range map不用考虑 range go原生map每次顺序随机,从而导致2次Encode相同数据后得到的结果不同) 203 // PS:如果不这么做,就要对map做转化,先变成 range 保序的 slice,再 Encode; 204 // 但测试的功能点是Encode,所以采用上述方法绕过range go原生map每次顺序随机的特点,完成功能测试 205 // 因子:对Encode后的数据进行Decode 206 // 因子:「构造的用例数据」的长度(len([]*models.PipelineGroupEvents))未超过 series limit 207 // 预期:「构造的用例数据」和「对用例数据先Encode再Decode后的数据」相等 208 func TestEncoderBatchV2_ShouldDecodeSuccess_GivenNormalDataOfDataOfPipelineGroupEventsWithSingleTagNotExceedingSeriesLimit(t *testing.T) { 209 // given 210 seriesLimit := 19 211 n := seriesLimit 212 wantGroupEventsSlice := genPipelineGroupEventsSliceSingleTag(n) 213 p := NewPromEncoder(seriesLimit) 214 data, err := p.EncodeBatchV2(wantGroupEventsSlice) 215 assert.NoError(t, err) 216 217 // when 218 // then 219 gotGroupEventsSlice, err := DecodeBatchV2(data) 220 assert.NoError(t, err) 221 assert.Equal(t, wantGroupEventsSlice, gotGroupEventsSlice) 222 } 223 224 // 场景:V2 Encode接口功能测试 225 // 说明:EncoderBatchV2 内部会调用 EncoderV2,所以也同时测试了 EncoderV2 的功能 226 // 因子:所有 Event type 均为 models.EventTypeMetric 227 // 因子:所有 PipelineEvent interface 的实现均为 *models.Metric 228 // 因子:每个 metric event 生成的 *models.Metric.Tags 中的 tag 仅一个 229 // (确保 Encode 时 range map不用考虑 range go原生map每次顺序随机,从而导致2次Encode相同数据后得到的结果不同) 230 // PS:如果不这么做,就要对map做转化,先变成 range 保序的 slice,再 Encode; 231 // 但测试的功能点是Encode,所以采用上述方法绕过range go原生map每次顺序随机的特点,完成功能测试 232 // 因子:对Encode后的数据进行Decode 233 // 因子:「构造的用例数据」的长度(len([]*models.PipelineGroupEvents))超过 series limit 234 // 预期:「构造的用例数据」的长度小于「对用例数据先Encode再Decode后的数据」的长度,且用 expectedLen 计算后的长度相等 235 // PS:expectedLen 的计算方法,其实是和 genPipelineGroupEventsSlice 生成用例及根据series limit确定encode批次 236 // 的逻辑相关,和 Encode 本身的逻辑无关 237 func TestEncoderBatchV2_ShouldDecodeSuccess_GivenNormalDataOfDataOfPipelineGroupEventsWithSingleTagExceedingSeriesLimit(t *testing.T) { 238 // given 239 seriesLimit := 19 240 n := 100 241 wantGroupEventsSlice := genPipelineGroupEventsSliceSingleTag(n) 242 assert.Equal(t, n, len(wantGroupEventsSlice)) 243 p := NewPromEncoder(seriesLimit) 244 data, err := p.EncodeBatchV2(wantGroupEventsSlice) 245 assert.NoError(t, err) 246 expectedLen := func(limit, length int) int { 247 // make sure limit > 0 && length > 0 248 if limit <= 0 || length <= 0 { 249 return -1 250 } 251 252 mod := length % limit 253 mul := length / limit 254 255 res := 0 256 for i := 0; i <= mul; i++ { 257 res += i * limit 258 } 259 res += (mul + 1) * mod 260 261 return res 262 } 263 264 // when 265 gotGroupEventsSlice, err := DecodeBatchV2(data) 266 assert.NoError(t, err) 267 268 // then 269 assert.Equal(t, expectedLen(seriesLimit, n), len(gotGroupEventsSlice)) 270 } 271 272 func genNormalPipelineGroupEventsSlice(n int) []*models.PipelineGroupEvents { 273 return genPipelineGroupEventsSlice(n, genPipelineEvent) 274 } 275 276 func genPipelineGroupEventsSliceIncludingAbnormalData(n int) []*models.PipelineGroupEvents { 277 return genPipelineGroupEventsSlice(n, genPipelineEventIncludingAbnormalData) 278 } 279 280 func genPipelineGroupEventsSliceSingleTag(n int) []*models.PipelineGroupEvents { 281 return genPipelineGroupEventsSlice(n, genPipelineEventSingleTag) 282 } 283 284 func genPipelineGroupEventsSlice(n int, genPipelineEventFn func(int) []models.PipelineEvent) []*models.PipelineGroupEvents { 285 res := make([]*models.PipelineGroupEvents, 0, n) 286 for i := 1; i <= n; i++ { 287 res = append(res, &models.PipelineGroupEvents{ 288 Group: models.NewGroup(models.NewMetadata(), models.NewTags()), 289 Events: genPipelineEventFn(i), 290 }) 291 } 292 293 return res 294 } 295 296 func genPipelineEvent(n int) []models.PipelineEvent { 297 res := make([]models.PipelineEvent, 0, n) 298 for i := 1; i <= n; i++ { 299 res = append(res, genMetric(i)) 300 } 301 302 return res 303 } 304 305 func genMetric(n int) *models.Metric { 306 i := strconv.Itoa(n) 307 tags := models.NewKeyValues[string]() 308 tags.AddAll(map[string]string{ 309 // range map will out of order 310 "a" + i: "A" + i, 311 "b" + i: "B" + i, 312 "c" + i: "C" + i, 313 "d" + i: "D" + i, 314 }) 315 316 return &models.Metric{ 317 Timestamp: 11111111 * uint64(n), 318 Tags: tags, 319 Value: &models.MetricSingleValue{Value: 1.1 * float64(n)}, 320 } 321 } 322 323 func genPipelineEventIncludingAbnormalData(n int) []models.PipelineEvent { 324 res := make([]models.PipelineEvent, 0, n) 325 for i := 1; i <= n; i++ { 326 if i&1 == 0 { // i is even number 327 // normal data 328 res = append(res, genMetric(i)) 329 continue 330 } 331 332 // i is odd number 333 // abnormal data 334 if i%3 == 0 { 335 // abnormal data: nil data 336 res = append(res, nil) 337 continue 338 } 339 340 if i%5 == 0 { 341 // abnormal data: zero data 342 // PS: 343 // 1. 这里只是从边界情况考虑,构造了这种异常值 344 // 但实际场景中,不会直接 new(models.Metric) 或者 &models.Metric{} 这样创建 zero data, 345 // 一般都是用 models.NewMetric|NewSingleValueMetric|NewMultiValuesMetric 等 构造函数(工厂模式)来创建, 346 // 上述构造函数位置:ilogtail/pkg/models/factory.go 347 // 2. 此外,也可以给 *models.Metric 的 GetTag 方法增加下 *models.Metric.Tag 为 nil 时的保护 348 // (参考其 GetValue 方法的实现),文件位置:ilogtail/pkg/models/metric.go 349 res = append(res, new(models.Metric)) 350 continue 351 } 352 353 // abnormal data: other event type not models.EventTypeMetric 354 res = append(res, new(models.Log)) 355 } 356 357 return res 358 } 359 360 func genPipelineEventSingleTag(n int) []models.PipelineEvent { 361 res := make([]models.PipelineEvent, 0, n) 362 for i := 1; i <= n; i++ { 363 res = append(res, genMetricSingleTag(i)) 364 } 365 366 return res 367 } 368 369 func genMetricSingleTag(n int) *models.Metric { 370 metricName := "test_metric" 371 i := strconv.Itoa(n) 372 tags := models.NewTagsWithMap(map[string]string{ 373 // only single tag 374 // keep range in order 375 "x" + i: "X" + i, 376 }) 377 378 dataPoint := pb.Sample{Timestamp: 11111111 * int64(n), Value: 1.1 * float64(n)} 379 380 return models.NewSingleValueMetric( 381 metricName, // value of key "__name__" in prometheus 382 models.MetricTypeGauge, 383 tags, 384 model.Time(dataPoint.Timestamp).Time().UnixNano(), 385 dataPoint.Value, 386 ) 387 } 388 389 func DecodeBatchV2(data [][]byte) ([]*models.PipelineGroupEvents, error) { 390 if len(data) == 0 { 391 return nil, errors.New("no data to decode") 392 } 393 394 var res []*models.PipelineGroupEvents 395 396 meta, commonTags := models.NewMetadata(), models.NewTags() 397 for _, d := range data { 398 groupEvents, err := convertPromRequestToPipelineGroupEvents(d, meta, commonTags) 399 if err != nil { 400 continue 401 } 402 403 res = append(res, groupEvents) 404 } 405 406 return res, nil 407 } 408 409 func convertPromRequestToPipelineGroupEvents(data []byte, metaInfo models.Metadata, commonTags models.Tags) (*models.PipelineGroupEvents, error) { 410 wr, err := unmarshalBatchTimeseriesData(data) 411 if err != nil { 412 return nil, err 413 } 414 415 groupEvent := &models.PipelineGroupEvents{ 416 Group: models.NewGroup(metaInfo, commonTags), 417 } 418 419 for _, ts := range wr.Timeseries { 420 var metricName string 421 tags := models.NewTags() 422 for _, label := range ts.Labels { 423 if label.Name == metricNameKey { 424 metricName = label.Value 425 continue 426 } 427 tags.Add(label.Name, label.Value) 428 } 429 430 for _, dataPoint := range ts.Samples { 431 metric := models.NewSingleValueMetric( 432 metricName, 433 models.MetricTypeGauge, 434 tags, 435 // Decode (during input_prometheus stage) makes timestamp 436 // with unix milliseconds into unix nanoseconds, 437 // e.g. "model.Time(milliseconds).Time().UnixNano()". 438 model.Time(dataPoint.Timestamp).Time().UnixNano(), 439 dataPoint.Value, 440 ) 441 groupEvent.Events = append(groupEvent.Events, metric) 442 } 443 } 444 445 return groupEvent, nil 446 } 447 448 func unmarshalBatchTimeseriesData(data []byte) (*pb.WriteRequest, error) { 449 wr := new(prompb.WriteRequest) 450 if err := wr.Unmarshal(data); err != nil { 451 return nil, err 452 } 453 454 return convertPrompbToVictoriaMetricspb(wr) 455 } 456 457 func convertPrompbToVictoriaMetricspb(wr *prompb.WriteRequest) (*pb.WriteRequest, error) { 458 if wr == nil || len(wr.Timeseries) == 0 { 459 return nil, errors.New("nil *prompb.WriteRequest") 460 } 461 462 res := &pb.WriteRequest{ 463 Timeseries: make([]pb.TimeSeries, 0, len(wr.Timeseries)), 464 } 465 for _, tss := range wr.Timeseries { 466 res.Timeseries = append(res.Timeseries, pb.TimeSeries{ 467 Labels: convertToVMLabels(tss.Labels), 468 Samples: convertToVMSamples(tss.Samples), 469 }) 470 } 471 472 return res, nil 473 } 474 475 func convertToVMLabels(labels []prompb.Label) []pb.Label { 476 if len(labels) == 0 { 477 return nil 478 } 479 480 res := make([]pb.Label, 0, len(labels)) 481 for _, label := range labels { 482 res = append(res, pb.Label{ 483 Name: string(label.Name), 484 Value: string(label.Value), 485 }) 486 } 487 488 return res 489 } 490 491 func convertToVMSamples(samples []prompb.Sample) []pb.Sample { 492 if len(samples) == 0 { 493 return nil 494 } 495 496 res := make([]pb.Sample, 0, len(samples)) 497 for _, sample := range samples { 498 res = append(res, pb.Sample{ 499 Value: sample.Value, 500 Timestamp: sample.Timestamp, 501 }) 502 } 503 504 return res 505 }