github.com/cilium/cilium@v1.16.2/pkg/hubble/exporter/dynamic_exporter_test.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 // Copyright Authors of Cilium 3 4 package exporter 5 6 import ( 7 "context" 8 "os" 9 "testing" 10 11 "github.com/prometheus/client_golang/prometheus" 12 "github.com/sirupsen/logrus" 13 "github.com/stretchr/testify/assert" 14 "github.com/stretchr/testify/require" 15 16 v1 "github.com/cilium/cilium/pkg/hubble/api/v1" 17 "github.com/cilium/cilium/pkg/time" 18 ) 19 20 var ( 21 future = time.Now().Add(1 * time.Hour) 22 past = time.Now().Add(-1 * time.Hour) 23 ) 24 25 func TestDynamicExporterLifecycle(t *testing.T) { 26 // given 27 fileName := "testdata/valid-flowlogs-config.yaml" 28 29 // when 30 sut := NewDynamicExporter(logrus.New(), fileName, 5, 1) 31 32 // then 33 assert.Len(t, sut.managedExporters, 3) 34 for _, v := range sut.managedExporters { 35 exp := v.exporter.(*exporter) 36 assert.NotNil(t, exp.writer, "each individual exporter should be configured (writer != nil)") 37 } 38 39 // and when 40 assert.NoError(t, sut.Stop()) 41 42 // then 43 assert.Len(t, sut.managedExporters, 3) 44 for _, v := range sut.managedExporters { 45 exp := v.exporter.(*exporter) 46 assert.Nil(t, exp.writer, "each individual exporter should be stopped (writer == nil)") 47 } 48 } 49 50 func TestAddNewExporter(t *testing.T) { 51 // given 52 sut := &DynamicExporter{ 53 logger: logrus.New(), 54 managedExporters: make(map[string]*managedExporter), 55 } 56 57 // and 58 file := createEmptyLogFile(t) 59 60 // and 61 config := DynamicExportersConfig{ 62 FlowLogs: []*FlowLogConfig{ 63 { 64 Name: "test001", 65 FilePath: file.Name(), 66 FieldMask: FieldMask{}, 67 IncludeFilters: FlowFilters{}, 68 ExcludeFilters: FlowFilters{}, 69 End: &future, 70 }, 71 }, 72 } 73 74 // when 75 sut.onConfigReload(context.TODO(), 1, config) 76 77 // then 78 assert.Equal(t, 1, len(sut.managedExporters)) 79 assert.Equal(t, config.FlowLogs[0], sut.managedExporters["test001"].config) 80 assert.NotNil(t, sut.managedExporters["test001"].exporter) 81 } 82 83 func TestConfigReloadChanges(t *testing.T) { 84 // given 85 sut := &DynamicExporter{ 86 logger: logrus.New(), 87 managedExporters: make(map[string]*managedExporter), 88 } 89 90 // and 91 file := createEmptyLogFile(t) 92 93 // and 94 config := DynamicExportersConfig{ 95 FlowLogs: []*FlowLogConfig{ 96 { 97 Name: "test001", 98 FilePath: file.Name(), 99 FieldMask: FieldMask{}, 100 IncludeFilters: FlowFilters{}, 101 ExcludeFilters: FlowFilters{}, 102 End: &future, 103 }, 104 }, 105 } 106 107 mockExporter := &mockExporter{} 108 sut.managedExporters["test001"] = &managedExporter{ 109 config: config.FlowLogs[0], 110 exporter: mockExporter, 111 } 112 113 // when 114 sut.onConfigReload(context.TODO(), 1, config) 115 116 // then 117 assert.False(t, mockExporter.stopped, "should not reload when not changed") 118 119 // and when 120 newConfig := DynamicExportersConfig{ 121 FlowLogs: []*FlowLogConfig{ 122 { 123 Name: "test001", 124 FilePath: file.Name(), 125 FieldMask: FieldMask{"source"}, 126 IncludeFilters: FlowFilters{}, 127 ExcludeFilters: FlowFilters{}, 128 End: &future, 129 }, 130 }, 131 } 132 sut.onConfigReload(context.TODO(), 1, newConfig) 133 134 // then 135 assert.True(t, mockExporter.stopped, "should reload when changed") 136 } 137 138 func TestEventPropagation(t *testing.T) { 139 // given 140 sut := &DynamicExporter{ 141 logger: logrus.New(), 142 managedExporters: make(map[string]*managedExporter), 143 } 144 145 // and 146 file := createEmptyLogFile(t) 147 148 // and 149 future := time.Now().Add(1 * time.Hour) 150 past := time.Now().Add(-1 * time.Hour) 151 config := DynamicExportersConfig{ 152 FlowLogs: []*FlowLogConfig{ 153 { 154 Name: "test001", 155 FilePath: file.Name(), 156 FieldMask: FieldMask{}, 157 IncludeFilters: FlowFilters{}, 158 ExcludeFilters: FlowFilters{}, 159 End: &future, 160 }, 161 { 162 Name: "test002", 163 FilePath: file.Name(), 164 FieldMask: FieldMask{}, 165 IncludeFilters: FlowFilters{}, 166 ExcludeFilters: FlowFilters{}, 167 End: &future, 168 }, 169 { 170 Name: "test003", 171 FilePath: file.Name(), 172 FieldMask: FieldMask{}, 173 IncludeFilters: FlowFilters{}, 174 ExcludeFilters: FlowFilters{}, 175 End: &past, 176 }, 177 { 178 Name: "test004", 179 FilePath: file.Name(), 180 FieldMask: FieldMask{}, 181 IncludeFilters: FlowFilters{}, 182 ExcludeFilters: FlowFilters{}, 183 End: nil, 184 }, 185 }, 186 } 187 188 mockExporter0 := &mockExporter{} 189 mockExporter1 := &mockExporter{} 190 mockExporter2 := &mockExporter{} 191 mockExporter3 := &mockExporter{} 192 sut.managedExporters["test001"] = &managedExporter{ 193 config: config.FlowLogs[0], 194 exporter: mockExporter0, 195 } 196 sut.managedExporters["test002"] = &managedExporter{ 197 config: config.FlowLogs[1], 198 exporter: mockExporter1, 199 } 200 sut.managedExporters["test003"] = &managedExporter{ 201 config: config.FlowLogs[2], 202 exporter: mockExporter2, 203 } 204 sut.managedExporters["test004"] = &managedExporter{ 205 config: config.FlowLogs[3], 206 exporter: mockExporter3, 207 } 208 209 // when 210 sut.OnDecodedEvent(context.TODO(), &v1.Event{}) 211 212 // then 213 assert.Equal(t, 1, mockExporter0.events) 214 assert.Equal(t, 1, mockExporter1.events) 215 assert.Equal(t, 0, mockExporter2.events) 216 assert.Equal(t, 1, mockExporter3.events) 217 } 218 219 func TestExporterReconfigurationMetricsReporting(t *testing.T) { 220 // given 221 registry := prometheus.NewRegistry() 222 DynamicExporterReconfigurations.Reset() 223 registry.MustRegister(DynamicExporterReconfigurations) 224 225 // and 226 sut := &DynamicExporter{ 227 logger: logrus.New(), 228 managedExporters: make(map[string]*managedExporter), 229 } 230 231 // and 232 file := createEmptyLogFile(t) 233 234 t.Run("should report flowlog added metric", func(t *testing.T) { 235 // given 236 config := DynamicExportersConfig{ 237 FlowLogs: []*FlowLogConfig{ 238 { 239 Name: "test001", 240 FilePath: file.Name(), 241 FieldMask: FieldMask{}, 242 IncludeFilters: FlowFilters{}, 243 ExcludeFilters: FlowFilters{}, 244 End: &future, 245 }, 246 }, 247 } 248 249 // when 250 sut.onConfigReload(context.TODO(), 1, config) 251 252 // then 253 metricFamilies, err := registry.Gather() 254 require.NoError(t, err) 255 require.Len(t, metricFamilies, 1) 256 257 assert.Equal(t, "hubble_dynamic_exporter_reconfigurations_total", *metricFamilies[0].Name) 258 require.Len(t, metricFamilies[0].Metric, 1) 259 metric := metricFamilies[0].Metric[0] 260 261 assert.Equal(t, "op", *metric.Label[0].Name) 262 assert.Equal(t, "add", *metric.Label[0].Value) 263 assert.Equal(t, float64(1), *metric.GetCounter().Value) 264 }) 265 266 t.Run("should report flowlog updated metric", func(t *testing.T) { 267 // given 268 config := DynamicExportersConfig{ 269 FlowLogs: []*FlowLogConfig{ 270 { 271 Name: "test001", 272 FilePath: file.Name(), 273 FieldMask: FieldMask{"source"}, 274 IncludeFilters: FlowFilters{}, 275 ExcludeFilters: FlowFilters{}, 276 End: &future, 277 }, 278 }, 279 } 280 281 // when 282 sut.onConfigReload(context.TODO(), 1, config) 283 284 // then 285 metricFamilies, err := registry.Gather() 286 require.NoError(t, err) 287 require.Len(t, metricFamilies, 1) 288 289 assert.Equal(t, "hubble_dynamic_exporter_reconfigurations_total", *metricFamilies[0].Name) 290 require.Len(t, metricFamilies[0].Metric, 2) 291 metric := metricFamilies[0].Metric[1] 292 293 assert.Equal(t, "op", *metric.Label[0].Name) 294 assert.Equal(t, "update", *metric.Label[0].Value) 295 assert.Equal(t, float64(1), *metric.GetCounter().Value) 296 }) 297 298 t.Run("should not increase flowlog updated metric when config not changed", func(t *testing.T) { 299 // given 300 config4 := DynamicExportersConfig{ 301 FlowLogs: []*FlowLogConfig{ 302 { 303 Name: "test001", 304 FilePath: file.Name(), 305 FieldMask: FieldMask{"source"}, 306 IncludeFilters: FlowFilters{}, 307 ExcludeFilters: FlowFilters{}, 308 End: &future, 309 }, 310 }, 311 } 312 313 // when 314 sut.onConfigReload(context.TODO(), 1, config4) 315 316 // then 317 metricFamilies, err := registry.Gather() 318 require.NoError(t, err) 319 require.Len(t, metricFamilies, 1) 320 321 assert.Equal(t, "hubble_dynamic_exporter_reconfigurations_total", *metricFamilies[0].Name) 322 require.Len(t, metricFamilies[0].Metric, 2) 323 metric := metricFamilies[0].Metric[1] 324 325 assert.Equal(t, "op", *metric.Label[0].Name) 326 assert.Equal(t, "update", *metric.Label[0].Value) 327 assert.Equal(t, float64(1), *metric.GetCounter().Value) 328 }) 329 330 t.Run("should report flowlog removed metric", func(t *testing.T) { 331 // given 332 config := DynamicExportersConfig{ 333 FlowLogs: []*FlowLogConfig{}, 334 } 335 336 // when 337 sut.onConfigReload(context.TODO(), 1, config) 338 339 // then 340 metricFamilies, err := registry.Gather() 341 require.NoError(t, err) 342 require.Len(t, metricFamilies, 1) 343 344 assert.Equal(t, "hubble_dynamic_exporter_reconfigurations_total", *metricFamilies[0].Name) 345 require.Len(t, metricFamilies[0].Metric, 3) 346 metric := metricFamilies[0].Metric[1] 347 348 assert.Equal(t, "op", *metric.Label[0].Name) 349 assert.Equal(t, "remove", *metric.Label[0].Value) 350 assert.Equal(t, float64(1), *metric.GetCounter().Value) 351 352 }) 353 } 354 355 func TestExporterReconfigurationHashMetricsReporting(t *testing.T) { 356 // given 357 registry := prometheus.NewRegistry() 358 DynamicExporterConfigHash.Reset() 359 DynamicExporterConfigLastApplied.Reset() 360 registry.MustRegister(DynamicExporterConfigHash, DynamicExporterConfigLastApplied) 361 362 // and 363 sut := &DynamicExporter{ 364 logger: logrus.New(), 365 managedExporters: make(map[string]*managedExporter), 366 } 367 368 // and 369 file := createEmptyLogFile(t) 370 371 // given 372 config := DynamicExportersConfig{ 373 FlowLogs: []*FlowLogConfig{ 374 { 375 Name: "test001", 376 FilePath: file.Name(), 377 FieldMask: FieldMask{}, 378 IncludeFilters: FlowFilters{}, 379 ExcludeFilters: FlowFilters{}, 380 End: &future, 381 }, 382 }, 383 } 384 385 //and 386 configHash := uint64(4367168) 387 388 // when 389 sut.onConfigReload(context.TODO(), configHash, config) 390 391 // then 392 metricFamilies, err := registry.Gather() 393 require.NoError(t, err) 394 require.Len(t, metricFamilies, 2) 395 396 assert.Equal(t, "hubble_dynamic_exporter_config_hash", *metricFamilies[0].Name) 397 require.Len(t, metricFamilies[0].Metric, 1) 398 hash := metricFamilies[0].Metric[0] 399 400 assert.Equal(t, float64(configHash), *hash.GetGauge().Value) 401 402 assert.Equal(t, "hubble_dynamic_exporter_config_last_applied", *metricFamilies[1].Name) 403 require.Len(t, metricFamilies[1].Metric, 1) 404 timestamp := metricFamilies[1].Metric[0] 405 assert.InDelta(t, time.Now().Unix(), *timestamp.GetGauge().Value, 1, "verify reconfiguration within 1 second from now") 406 } 407 408 func TestExportersMetricsReporting(t *testing.T) { 409 // given 410 sut := &DynamicExporter{ 411 logger: logrus.New(), 412 managedExporters: make(map[string]*managedExporter), 413 } 414 415 // and 416 registry := prometheus.NewRegistry() 417 registry.MustRegister(&dynamicExporterGaugeCollector{exporter: sut}) 418 419 // and 420 file := createEmptyLogFile(t) 421 422 t.Run("should report gauge with exporters statuses", func(t *testing.T) { 423 // given 424 config := DynamicExportersConfig{ 425 FlowLogs: []*FlowLogConfig{ 426 { 427 Name: "test001", 428 FilePath: file.Name(), 429 FieldMask: FieldMask{}, 430 IncludeFilters: FlowFilters{}, 431 ExcludeFilters: FlowFilters{}, 432 End: &future, 433 }, 434 { 435 Name: "test002", 436 FilePath: file.Name(), 437 FieldMask: FieldMask{}, 438 IncludeFilters: FlowFilters{}, 439 ExcludeFilters: FlowFilters{}, 440 End: &past, 441 }, 442 }, 443 } 444 445 // when 446 sut.onConfigReload(context.TODO(), 1, config) 447 448 // then 449 metricFamilies, err := registry.Gather() 450 require.NoError(t, err) 451 require.Len(t, metricFamilies, 2) 452 453 assert.Equal(t, "hubble_dynamic_exporter_exporters_total", *metricFamilies[0].Name) 454 require.Len(t, metricFamilies[0].Metric, 2) 455 456 assert.Equal(t, "status", *metricFamilies[0].Metric[0].Label[0].Name) 457 assert.Equal(t, "active", *metricFamilies[0].Metric[0].Label[0].Value) 458 assert.Equal(t, float64(1), *metricFamilies[0].Metric[0].GetGauge().Value) 459 460 assert.Equal(t, "status", *metricFamilies[0].Metric[1].Label[0].Name) 461 assert.Equal(t, "inactive", *metricFamilies[0].Metric[1].Label[0].Value) 462 assert.Equal(t, float64(1), *metricFamilies[0].Metric[1].GetGauge().Value) 463 464 assert.Equal(t, "hubble_dynamic_exporter_up", *metricFamilies[1].Name) 465 require.Len(t, metricFamilies[1].Metric, 2) 466 467 assert.Equal(t, "name", *metricFamilies[1].Metric[0].Label[0].Name) 468 assert.Equal(t, "test001", *metricFamilies[1].Metric[0].Label[0].Value) 469 assert.Equal(t, float64(1), *metricFamilies[1].Metric[0].GetGauge().Value) 470 471 assert.Equal(t, "name", *metricFamilies[1].Metric[1].Label[0].Name) 472 assert.Equal(t, "test002", *metricFamilies[1].Metric[1].Label[0].Value) 473 assert.Equal(t, float64(0), *metricFamilies[1].Metric[1].GetGauge().Value) 474 }) 475 476 t.Run("should remove individual status metric of removed flowlog", func(t *testing.T) { 477 // given 478 config := DynamicExportersConfig{ 479 FlowLogs: []*FlowLogConfig{}, 480 } 481 482 // when 483 sut.onConfigReload(context.TODO(), 1, config) 484 485 // then 486 metricFamilies, err := registry.Gather() 487 require.NoError(t, err) 488 require.Len(t, metricFamilies, 1) 489 490 assert.Equal(t, "hubble_dynamic_exporter_exporters_total", *metricFamilies[0].Name) 491 require.Len(t, metricFamilies[0].Metric, 2) 492 493 assert.Equal(t, "status", *metricFamilies[0].Metric[0].Label[0].Name) 494 assert.Equal(t, "active", *metricFamilies[0].Metric[0].Label[0].Value) 495 assert.Equal(t, float64(0), *metricFamilies[0].Metric[0].GetGauge().Value) 496 497 assert.Equal(t, "status", *metricFamilies[0].Metric[1].Label[0].Name) 498 assert.Equal(t, "inactive", *metricFamilies[0].Metric[1].Label[0].Value) 499 assert.Equal(t, float64(0), *metricFamilies[0].Metric[1].GetGauge().Value) 500 }) 501 } 502 503 func createEmptyLogFile(t *testing.T) *os.File { 504 file, err := os.CreateTemp(t.TempDir(), "output.log") 505 if err != nil { 506 t.Fatalf("failed creating test file %v", err) 507 } 508 509 return file 510 } 511 512 type mockExporter struct { 513 events int 514 stopped bool 515 } 516 517 func (m *mockExporter) Stop() error { 518 m.stopped = true 519 return nil 520 } 521 522 func (m *mockExporter) OnDecodedEvent(_ context.Context, _ *v1.Event) (bool, error) { 523 m.events++ 524 return false, nil 525 }