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  }