From e69481bfd0ee41e26a0eb16c478549adb9745851 Mon Sep 17 00:00:00 2001 From: Biriukov Sergei Date: Wed, 1 Feb 2023 16:27:08 +1100 Subject: [PATCH 1/4] Consolidate metrics code in a single package, add metrics prefix config option (#3) --- deploy/01-config.yaml | 3 +- main.go | 28 +++----------- pkg/exporter/channel_registry.go | 13 ++----- pkg/exporter/config.go | 24 ++++++++++++ pkg/exporter/config_test.go | 59 ++++++++++++++++++++++++++++- pkg/exporter/engine.go | 3 +- pkg/kube/watcher.go | 35 ++++++----------- pkg/kube/watcher_test.go | 32 +++++++++++++--- pkg/metrics/metrics.go | 65 ++++++++++++++++++++++++++++++++ 9 files changed, 197 insertions(+), 65 deletions(-) create mode 100644 pkg/metrics/metrics.go diff --git a/deploy/01-config.yaml b/deploy/01-config.yaml index fac7452a..bbbed53b 100644 --- a/deploy/01-config.yaml +++ b/deploy/01-config.yaml @@ -5,8 +5,9 @@ metadata: namespace: monitoring data: config.yaml: | - logLevel: error + logLevel: warn logFormat: json + metricsNamePrefix: event_exporter_ route: routes: - match: diff --git a/main.go b/main.go index 1d072f63..1d259243 100644 --- a/main.go +++ b/main.go @@ -4,17 +4,14 @@ import ( "context" "flag" "io/ioutil" - "net/http" "os" "os/signal" "syscall" "time" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/collectors" - "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/resmoio/kubernetes-event-exporter/pkg/exporter" "github.com/resmoio/kubernetes-event-exporter/pkg/kube" + "github.com/resmoio/kubernetes-event-exporter/pkg/metrics" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "gopkg.in/yaml.v2" @@ -74,7 +71,10 @@ func main() { kubeconfig.QPS = cfg.KubeQPS kubeconfig.Burst = cfg.KubeBurst - engine := exporter.NewEngine(&cfg, &exporter.ChannelBasedReceiverRegistry{}) + metrics.Init(*addr) + metricsStore := metrics.NewMetricsStore(cfg.MetricsNamePrefix) + + engine := exporter.NewEngine(&cfg, &exporter.ChannelBasedReceiverRegistry{MetricsStore: metricsStore}) onEvent := engine.OnEvent if len(cfg.ClusterName) != 0 { onEvent = func(event *kube.EnhancedEvent) { @@ -84,7 +84,7 @@ func main() { engine.OnEvent(event) } } - w := kube.NewEventWatcher(kubeconfig, cfg.Namespace, cfg.MaxEventAgeSeconds, onEvent) + w := kube.NewEventWatcher(kubeconfig, cfg.Namespace, cfg.MaxEventAgeSeconds, metricsStore, onEvent) ctx, cancel := context.WithCancel(context.Background()) leaderLost := make(chan bool) @@ -107,22 +107,6 @@ func main() { w.Start() } - // Setup the prometheus metrics machinery - // Add Go module build info. - prometheus.MustRegister(collectors.NewBuildInfoCollector()) - - // Expose the registered metrics via HTTP. - http.Handle("/metrics", promhttp.HandlerFor( - prometheus.DefaultGatherer, - promhttp.HandlerOpts{ - // Opt into OpenMetrics to support exemplars. - EnableOpenMetrics: true, - }, - )) - - // start up the http listener to expose the metrics - go http.ListenAndServe(*addr, nil) - c := make(chan os.Signal, 1) signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) diff --git a/pkg/exporter/channel_registry.go b/pkg/exporter/channel_registry.go index 3263d29e..39db655a 100644 --- a/pkg/exporter/channel_registry.go +++ b/pkg/exporter/channel_registry.go @@ -5,19 +5,11 @@ import ( "sync" "github.com/resmoio/kubernetes-event-exporter/pkg/kube" + "github.com/resmoio/kubernetes-event-exporter/pkg/metrics" "github.com/resmoio/kubernetes-event-exporter/pkg/sinks" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promauto" "github.com/rs/zerolog/log" ) -var ( - sendErrors = promauto.NewCounter(prometheus.CounterOpts{ - Name: "send_event_errors", - Help: "The total number of send event errors", - }) -) - // ChannelBasedReceiverRegistry creates two channels for each receiver. One is for receiving events and other one is // for breaking out of the infinite loop. Each message is passed to receivers // This might not be the best way to implement such feature. A ring buffer can be better @@ -27,6 +19,7 @@ type ChannelBasedReceiverRegistry struct { ch map[string]chan kube.EnhancedEvent exitCh map[string]chan interface{} wg *sync.WaitGroup + MetricsStore *metrics.Store } func (r *ChannelBasedReceiverRegistry) SendEvent(name string, event *kube.EnhancedEvent) { @@ -65,7 +58,7 @@ func (r *ChannelBasedReceiverRegistry) Register(name string, receiver sinks.Sink log.Debug().Str("sink", name).Str("event", ev.Message).Msg("sending event to sink") err := receiver.Send(context.Background(), &ev) if err != nil { - sendErrors.Inc() + r.MetricsStore.SendErrors.Inc() log.Debug().Err(err).Str("sink", name).Str("event", ev.Message).Msg("Cannot send event") } case <-exitCh: diff --git a/pkg/exporter/config.go b/pkg/exporter/config.go index e75f4c49..baabcbb9 100644 --- a/pkg/exporter/config.go +++ b/pkg/exporter/config.go @@ -2,6 +2,7 @@ package exporter import ( "errors" + "regexp" "strconv" "github.com/resmoio/kubernetes-event-exporter/pkg/kube" @@ -25,12 +26,16 @@ type Config struct { Receivers []sinks.ReceiverConfig `yaml:"receivers"` KubeQPS float32 `yaml:"kubeQPS,omitempty"` KubeBurst int `yaml:"kubeBurst,omitempty"` + MetricsNamePrefix string `yaml:"metricsNamePrefix,omitempty"` } func (c *Config) Validate() error { if err := c.validateDefaults(); err != nil { return err } + if err := c.validateMetricsNamePrefix(); err != nil { + return err + } // No duplicate receivers // Receivers individually @@ -63,3 +68,22 @@ func (c *Config) validateMaxEventAgeSeconds() error { } return nil } + +func (c *Config) validateMetricsNamePrefix() error { + if c.MetricsNamePrefix != "" { + // https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels + checkResult, err := regexp.MatchString("^[a-zA-Z][a-zA-Z0-9_:]*_$", c.MetricsNamePrefix) + if err != nil { + return err + } + if checkResult { + log.Info().Msg("config.metricsNamePrefix='" + c.MetricsNamePrefix + "'") + } else { + log.Error().Msg("config.metricsNamePrefix should match the regex: ^[a-zA-Z][a-zA-Z0-9_:]*_$") + return errors.New("validateMetricsNamePrefix failed") + } + } else { + log.Warn().Msg("metrics name prefix is empty, setting config.metricsNamePrefix='event_exporter_' is recommended") + } + return nil +} diff --git a/pkg/exporter/config_test.go b/pkg/exporter/config_test.go index 0e5e8a17..01a6e629 100644 --- a/pkg/exporter/config_test.go +++ b/pkg/exporter/config_test.go @@ -91,4 +91,61 @@ func TestValidate_IsCheckingMaxEventAgeSeconds_WhenMaxEventAgeSecondsAndThrottle err := config.Validate() assert.Error(t, err) assert.Contains(t, output.String(), "cannot set both throttlePeriod (depricated) and MaxEventAgeSeconds") -} \ No newline at end of file +} + +func TestValidate_MetricsNamePrefix_WhenEmpty(t *testing.T) { + output := &bytes.Buffer{} + log.Logger = log.Logger.Output(output) + + config := Config{} + err := config.Validate() + assert.NoError(t, err) + assert.Equal(t, "", config.MetricsNamePrefix) + assert.Contains(t, output.String(), "metrics name prefix is empty, setting config.metricsNamePrefix='event_exporter_' is recommended") +} + +func TestValidate_MetricsNamePrefix_WhenValid(t *testing.T) { + output := &bytes.Buffer{} + log.Logger = log.Logger.Output(output) + + validCases := []string{ + "kubernetes_event_exporter_", + "test_", + "test_test_", + "test::test_test_", + "TEST::test_test_", + "test_test::1234_test_", + } + + for _, testPrefix := range validCases { + config := Config{ + MetricsNamePrefix: testPrefix, + } + err := config.Validate() + assert.NoError(t, err) + assert.Equal(t, testPrefix, config.MetricsNamePrefix) + assert.Contains(t, output.String(), "config.metricsNamePrefix='" + testPrefix + "'") + } +} + +func TestValidate_MetricsNamePrefix_WhenInvalid(t *testing.T) { + output := &bytes.Buffer{} + log.Logger = log.Logger.Output(output) + + invalidCases := []string{ + "no_tracing_underscore", + "__reserved_", + "::wrong_", + "13245_test_", + } + + for _, testPrefix := range invalidCases { + config := Config{ + MetricsNamePrefix: testPrefix, + } + err := config.Validate() + assert.Error(t, err) + assert.Equal(t, testPrefix, config.MetricsNamePrefix) + assert.Contains(t, output.String(), "config.metricsNamePrefix should match the regex: ^[a-zA-Z][a-zA-Z0-9_:]*_$") + } +} diff --git a/pkg/exporter/engine.go b/pkg/exporter/engine.go index 538cb706..c93b4ff2 100644 --- a/pkg/exporter/engine.go +++ b/pkg/exporter/engine.go @@ -1,9 +1,10 @@ package exporter import ( + "reflect" + "github.com/resmoio/kubernetes-event-exporter/pkg/kube" "github.com/rs/zerolog/log" - "reflect" ) // Engine is responsible for initializing the receivers from sinks diff --git a/pkg/kube/watcher.go b/pkg/kube/watcher.go index 5ad1e762..9d0cb59f 100644 --- a/pkg/kube/watcher.go +++ b/pkg/kube/watcher.go @@ -3,8 +3,7 @@ package kube import ( "time" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promauto" + "github.com/resmoio/kubernetes-event-exporter/pkg/metrics" "github.com/rs/zerolog/log" corev1 "k8s.io/api/core/v1" "k8s.io/client-go/informers" @@ -13,22 +12,7 @@ import ( "k8s.io/client-go/tools/cache" ) -var ( - eventsProcessed = promauto.NewCounter(prometheus.CounterOpts{ - Name: "events_sent", - Help: "The total number of events sent", - }) - eventsDiscarded = promauto.NewCounter(prometheus.CounterOpts{ - Name: "events_discarded", - Help: "The total number of events discarded because of being older than the maxEventAgeSeconds specified", - }) - watchErrors = promauto.NewCounter(prometheus.CounterOpts{ - Name: "watch_errors", - Help: "The total number of errors received from the informer", - }) - - startUpTime = time.Now() -) +var startUpTime = time.Now() type EventHandler func(event *EnhancedEvent) @@ -39,9 +23,10 @@ type EventWatcher struct { annotationCache *AnnotationCache fn EventHandler maxEventAgeSeconds time.Duration + metricsStore *metrics.Store } -func NewEventWatcher(config *rest.Config, namespace string, MaxEventAgeSeconds int64, fn EventHandler) *EventWatcher { +func NewEventWatcher(config *rest.Config, namespace string, MaxEventAgeSeconds int64, metricsStore *metrics.Store, fn EventHandler) *EventWatcher { clientset := kubernetes.NewForConfigOrDie(config) factory := informers.NewSharedInformerFactoryWithOptions(clientset, 0, informers.WithNamespace(namespace)) informer := factory.Core().V1().Events().Informer() @@ -53,11 +38,12 @@ func NewEventWatcher(config *rest.Config, namespace string, MaxEventAgeSeconds i annotationCache: NewAnnotationCache(config), fn: fn, maxEventAgeSeconds: time.Second * time.Duration(MaxEventAgeSeconds), + metricsStore: metricsStore, } informer.AddEventHandler(watcher) informer.SetWatchErrorHandler(func(r *cache.Reflector, err error) { - watchErrors.Inc() + watcher.metricsStore.WatchErrors.Inc() }) return watcher @@ -88,7 +74,7 @@ func (e *EventWatcher) isEventDiscarded(event *corev1.Event) bool { Str("event namespace", event.Namespace). Str("event name", event.Name). Msg("Event discarded as being older then maxEventAgeSeconds") - eventsDiscarded.Inc() + e.metricsStore.EventsDiscarded.Inc() } return true } @@ -107,7 +93,7 @@ func (e *EventWatcher) onEvent(event *corev1.Event) { Str("involvedObject", event.InvolvedObject.Name). Msg("Received event") - eventsProcessed.Inc() + e.metricsStore.EventsProcessed.Inc() ev := &EnhancedEvent{ Event: *event.DeepCopy(), @@ -155,16 +141,17 @@ func (e *EventWatcher) Stop() { close(e.stopper) } -func NewMockEventWatcher(MaxEventAgeSeconds int64) *EventWatcher { +func NewMockEventWatcher(MaxEventAgeSeconds int64, metricsStore *metrics.Store) *EventWatcher { watcher := &EventWatcher{ labelCache: NewMockLabelCache(), annotationCache: NewMockAnnotationCache(), maxEventAgeSeconds: time.Second * time.Duration(MaxEventAgeSeconds), fn: func(event *EnhancedEvent) {}, + metricsStore: metricsStore, } return watcher } -func (e *EventWatcher) SetStartUpTime(time time.Time) { +func (e *EventWatcher) setStartUpTime(time time.Time) { startUpTime = time } diff --git a/pkg/kube/watcher_test.go b/pkg/kube/watcher_test.go index 28b6359f..3bec350d 100644 --- a/pkg/kube/watcher_test.go +++ b/pkg/kube/watcher_test.go @@ -5,6 +5,8 @@ import ( "testing" "time" + "github.com/prometheus/client_golang/prometheus/testutil" + "github.com/resmoio/kubernetes-event-exporter/pkg/metrics" "github.com/rs/zerolog/log" "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" @@ -14,13 +16,14 @@ import ( func TestEventWatcher_EventAge_whenEventCreatedBeforeStartup(t *testing.T) { // should not discard events as old as 300s=5m var MaxEventAgeSeconds int64 = 300 - ew := NewMockEventWatcher(MaxEventAgeSeconds) + metricsStore := metrics.NewMetricsStore("test_") + ew := NewMockEventWatcher(MaxEventAgeSeconds, metricsStore) output := &bytes.Buffer{} log.Logger = log.Logger.Output(output) // event is 3m before stratup time -> expect silently dropped startup := time.Now().Add(-10 * time.Minute) - ew.SetStartUpTime(startup) + ew.setStartUpTime(startup) event1 := corev1.Event{ LastTimestamp: metav1.Time{Time: startup.Add(-3 * time.Minute)}, } @@ -30,6 +33,7 @@ func TestEventWatcher_EventAge_whenEventCreatedBeforeStartup(t *testing.T) { assert.NotContains(t, output.String(), "Event discarded as being older then maxEventAgeSeconds") ew.onEvent(&event1) assert.NotContains(t, output.String(), "Received event") + assert.Equal(t, float64(0), testutil.ToFloat64(metricsStore.EventsProcessed)) event2 := corev1.Event{ EventTime: metav1.MicroTime{Time: startup.Add(-3 * time.Minute)}, @@ -39,6 +43,7 @@ func TestEventWatcher_EventAge_whenEventCreatedBeforeStartup(t *testing.T) { assert.NotContains(t, output.String(), "Event discarded as being older then maxEventAgeSeconds") ew.onEvent(&event2) assert.NotContains(t, output.String(), "Received event") + assert.Equal(t, float64(0), testutil.ToFloat64(metricsStore.EventsProcessed)) // event is 3m before stratup time -> expect silently dropped event3 := corev1.Event{ @@ -50,17 +55,21 @@ func TestEventWatcher_EventAge_whenEventCreatedBeforeStartup(t *testing.T) { assert.NotContains(t, output.String(), "Event discarded as being older then maxEventAgeSeconds") ew.onEvent(&event3) assert.NotContains(t, output.String(), "Received event") + assert.Equal(t, float64(0), testutil.ToFloat64(metricsStore.EventsProcessed)) + + metrics.DestroyMetricsStore(metricsStore) } func TestEventWatcher_EventAge_whenEventCreatedAfterStartupAndBeforeMaxAge(t *testing.T) { // should not discard events as old as 300s=5m var MaxEventAgeSeconds int64 = 300 - ew := NewMockEventWatcher(MaxEventAgeSeconds) + metricsStore := metrics.NewMetricsStore("test_") + ew := NewMockEventWatcher(MaxEventAgeSeconds, metricsStore) output := &bytes.Buffer{} log.Logger = log.Logger.Output(output) startup := time.Now().Add(-10 * time.Minute) - ew.SetStartUpTime(startup) + ew.setStartUpTime(startup) // event is 8m after stratup time (2m in max age) -> expect processed event1 := corev1.Event{ InvolvedObject: corev1.ObjectReference{ @@ -75,6 +84,7 @@ func TestEventWatcher_EventAge_whenEventCreatedAfterStartupAndBeforeMaxAge(t *te ew.onEvent(&event1) assert.Contains(t, output.String(), "test-1") assert.Contains(t, output.String(), "Received event") + assert.Equal(t, float64(1), testutil.ToFloat64(metricsStore.EventsProcessed)) // event is 8m after stratup time (2m in max age) -> expect processed event2 := corev1.Event{ @@ -90,6 +100,7 @@ func TestEventWatcher_EventAge_whenEventCreatedAfterStartupAndBeforeMaxAge(t *te ew.onEvent(&event2) assert.Contains(t, output.String(), "test-2") assert.Contains(t, output.String(), "Received event") + assert.Equal(t, float64(2), testutil.ToFloat64(metricsStore.EventsProcessed)) // event is 8m after stratup time (2m in max age) -> expect processed event3 := corev1.Event{ @@ -106,18 +117,22 @@ func TestEventWatcher_EventAge_whenEventCreatedAfterStartupAndBeforeMaxAge(t *te ew.onEvent(&event3) assert.Contains(t, output.String(), "test-3") assert.Contains(t, output.String(), "Received event") + assert.Equal(t, float64(3), testutil.ToFloat64(metricsStore.EventsProcessed)) + + metrics.DestroyMetricsStore(metricsStore) } func TestEventWatcher_EventAge_whenEventCreatedAfterStartupAndAfterMaxAge(t *testing.T) { // should not discard events as old as 300s=5m var MaxEventAgeSeconds int64 = 300 - ew := NewMockEventWatcher(MaxEventAgeSeconds) + metricsStore := metrics.NewMetricsStore("test_") + ew := NewMockEventWatcher(MaxEventAgeSeconds, metricsStore) output := &bytes.Buffer{} log.Logger = log.Logger.Output(output) // event is 3m after stratup time (and 2m after max age) -> expect dropped with warn startup := time.Now().Add(-10 * time.Minute) - ew.SetStartUpTime(startup) + ew.setStartUpTime(startup) event1 := corev1.Event{ ObjectMeta: metav1.ObjectMeta{Name: "event1"}, LastTimestamp: metav1.Time{Time: startup.Add(3 * time.Minute)}, @@ -127,6 +142,7 @@ func TestEventWatcher_EventAge_whenEventCreatedAfterStartupAndAfterMaxAge(t *tes assert.Contains(t, output.String(), "Event discarded as being older then maxEventAgeSeconds") ew.onEvent(&event1) assert.NotContains(t, output.String(), "Received event") + assert.Equal(t, float64(0), testutil.ToFloat64(metricsStore.EventsProcessed)) // event is 3m after stratup time (and 2m after max age) -> expect dropped with warn event2 := corev1.Event{ @@ -139,6 +155,7 @@ func TestEventWatcher_EventAge_whenEventCreatedAfterStartupAndAfterMaxAge(t *tes assert.Contains(t, output.String(), "Event discarded as being older then maxEventAgeSeconds") ew.onEvent(&event2) assert.NotContains(t, output.String(), "Received event") + assert.Equal(t, float64(0), testutil.ToFloat64(metricsStore.EventsProcessed)) // event is 3m after stratup time (and 2m after max age) -> expect dropped with warn event3 := corev1.Event{ @@ -152,4 +169,7 @@ func TestEventWatcher_EventAge_whenEventCreatedAfterStartupAndAfterMaxAge(t *tes assert.Contains(t, output.String(), "Event discarded as being older then maxEventAgeSeconds") ew.onEvent(&event3) assert.NotContains(t, output.String(), "Received event") + assert.Equal(t, float64(0), testutil.ToFloat64(metricsStore.EventsProcessed)) + + metrics.DestroyMetricsStore(metricsStore) } diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go new file mode 100644 index 00000000..cd10e83d --- /dev/null +++ b/pkg/metrics/metrics.go @@ -0,0 +1,65 @@ +package metrics + +import ( + "net/http" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/collectors" + "github.com/prometheus/client_golang/prometheus/promauto" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + + +type Store struct { + EventsProcessed prometheus.Counter + EventsDiscarded prometheus.Counter + WatchErrors prometheus.Counter + SendErrors prometheus.Counter +} + +func Init(addr string) { + // Setup the prometheus metrics machinery + // Add Go module build info. + prometheus.MustRegister(collectors.NewBuildInfoCollector()) + + // Expose the registered metrics via HTTP. + http.Handle("/metrics", promhttp.HandlerFor( + prometheus.DefaultGatherer, + promhttp.HandlerOpts{ + // Opt into OpenMetrics to support exemplars. + EnableOpenMetrics: true, + }, + )) + + // start up the http listener to expose the metrics + go http.ListenAndServe(addr, nil) +} + +func NewMetricsStore(name_prefix string) *Store { + return &Store{ + EventsProcessed: promauto.NewCounter(prometheus.CounterOpts{ + Name: name_prefix + "events_sent", + Help: "The total number of events processed", + }), + EventsDiscarded: promauto.NewCounter(prometheus.CounterOpts{ + Name: name_prefix + "events_discarded", + Help: "The total number of events discarded because of being older than the maxEventAgeSeconds specified", + }), + WatchErrors: promauto.NewCounter(prometheus.CounterOpts{ + Name: name_prefix + "watch_errors", + Help: "The total number of errors received from the informer", + }), + SendErrors: promauto.NewCounter(prometheus.CounterOpts{ + Name: name_prefix + "send_event_errors", + Help: "The total number of send event errors", + }), + } +} + +func DestroyMetricsStore(store *Store) { + prometheus.Unregister(store.EventsProcessed) + prometheus.Unregister(store.EventsDiscarded) + prometheus.Unregister(store.WatchErrors) + prometheus.Unregister(store.SendErrors) + store = nil +} From 9d624eabb074411cda88e00369728ab606d48bd6 Mon Sep 17 00:00:00 2001 From: Evedel Date: Sun, 19 Mar 2023 16:05:30 +1100 Subject: [PATCH 2/4] fix incorrect docs --- README.md | 14 +++++++------- config.example.yaml | 8 ++++---- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 615faaf7..ae65fcd0 100644 --- a/README.md +++ b/README.md @@ -329,13 +329,13 @@ receivers: username: "kube-event-producer" passsord: "kube-event-producer-password" layout: #optionnal - kind: {{ .InvolvedObject.Kind }} - namespace: {{ .InvolvedObject.Namespace }} - name: {{ .InvolvedObject.Name }} - reason: {{ .Reason }} - message: {{ .Message }} - type: {{ .Type }} - createdAt: {{ .GetTimestampISO8601 }} + kind: "{{ .InvolvedObject.Kind }}" + namespace: "{{ .InvolvedObject.Namespace }}" + name: "{{ .InvolvedObject.Name }}" + reason: "{{ .Reason }}" + message: "{{ .Message }}" + type: "{{ .Type }}" + createdAt: "{{ .GetTimestampISO8601 }}" ``` ### OpsCenter diff --git a/config.example.yaml b/config.example.yaml index e5bce965..2ef9eeb9 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -33,10 +33,10 @@ receivers: - "http://localhost:9200" indexFormat: "kube-events-{2006-01-02}" - name: "opensearch-dump" - opensearch: - hosts: - - "http://localhost:9200" - indexFormat: "kube-events-{2006-01-02}" + opensearch: + hosts: + - "http://localhost:9200" + indexFormat: "kube-events-{2006-01-02}" - name: "alert" opsgenie: apiKey: "" From 94ee2b7e0371b743acde0de72323b3c40f01fab3 Mon Sep 17 00:00:00 2001 From: Evedel Date: Sun, 19 Mar 2023 16:10:21 +1100 Subject: [PATCH 3/4] install goccy/go-yaml --- go.mod | 7 +++++-- go.sum | 20 ++++++++++++++++++++ pkg/exporter/config_test.go | 5 ++--- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 3a18efc6..5ced9468 100644 --- a/go.mod +++ b/go.mod @@ -25,6 +25,8 @@ require ( k8s.io/client-go v0.26.0 ) +require github.com/fatih/color v1.15.0 // indirect + require ( cloud.google.com/go v0.107.0 // indirect cloud.google.com/go/compute v1.14.0 // indirect @@ -43,6 +45,7 @@ require ( github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.20.0 // indirect github.com/go-openapi/swag v0.22.3 // indirect + github.com/goccy/go-yaml v1.10.0 github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.2 // indirect @@ -72,7 +75,7 @@ require ( github.com/klauspost/compress v1.15.13 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.16 // indirect + github.com/mattn/go-isatty v0.0.17 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect @@ -94,7 +97,7 @@ require ( golang.org/x/net v0.7.0 // indirect golang.org/x/oauth2 v0.3.0 // indirect golang.org/x/sync v0.1.0 // indirect - golang.org/x/sys v0.5.0 // indirect + golang.org/x/sys v0.6.0 // indirect golang.org/x/term v0.5.0 // indirect golang.org/x/text v0.7.0 // indirect golang.org/x/time v0.3.0 // indirect diff --git a/go.sum b/go.sum index e15b46d7..14268ef5 100644 --- a/go.sum +++ b/go.sum @@ -65,6 +65,10 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= +github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg= +github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= +github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= +github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/flowstack/go-jsonschema v0.1.1/go.mod h1:yL7fNggx1o8rm9RlgXv7hTBWxdBM0rVwpMwimd3F3N0= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= @@ -79,8 +83,14 @@ github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXym github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho= github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/goccy/go-yaml v1.10.0 h1:rBi+5HGuznOxx0JZ+60LDY85gc0dyIJCIMvsMJTKSKQ= +github.com/goccy/go-yaml v1.10.0/go.mod h1:h/18Lr6oSQ3mvmqFoWmQ47KChOgpfHpTyIHl3yVmpiY= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= @@ -191,18 +201,23 @@ github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/linkedin/goavro/v2 v2.12.0 h1:rIQQSj8jdAUlKQh6DttK8wCRv4t4QO09g1C4aBWXslg= github.com/linkedin/goavro/v2 v2.12.0/go.mod h1:KXx+erlq+RPlGSPmLF7xGo6SAbh8sCQ53x064+ioxhk= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= @@ -333,6 +348,8 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -342,6 +359,7 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -349,6 +367,8 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= diff --git a/pkg/exporter/config_test.go b/pkg/exporter/config_test.go index 01a6e629..475358e9 100644 --- a/pkg/exporter/config_test.go +++ b/pkg/exporter/config_test.go @@ -4,9 +4,9 @@ import ( "bytes" "testing" + "github.com/goccy/go-yaml" "github.com/rs/zerolog/log" "github.com/stretchr/testify/assert" - "gopkg.in/yaml.v2" ) func readConfig(t *testing.T, yml string) Config { @@ -44,7 +44,6 @@ receivers: assert.Equal(t, "stdout", cfg.Route.Routes[0].Match[0].Receiver) } - func TestValidate_IsCheckingMaxEventAgeSeconds_WhenNotSet(t *testing.T) { config := Config{} err := config.Validate() @@ -124,7 +123,7 @@ func TestValidate_MetricsNamePrefix_WhenValid(t *testing.T) { err := config.Validate() assert.NoError(t, err) assert.Equal(t, testPrefix, config.MetricsNamePrefix) - assert.Contains(t, output.String(), "config.metricsNamePrefix='" + testPrefix + "'") + assert.Contains(t, output.String(), "config.metricsNamePrefix='"+testPrefix+"'") } } From 7b93286ac5761fb199261df01564eff79e3fc745 Mon Sep 17 00:00:00 2001 From: Evedel Date: Sun, 19 Mar 2023 16:13:28 +1100 Subject: [PATCH 4/4] add setup package to handle errors during yaml parsing --- main.go | 18 ++++----- pkg/setup/setup.go | 33 ++++++++++++++++ pkg/setup/setup_test.go | 87 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 129 insertions(+), 9 deletions(-) create mode 100644 pkg/setup/setup.go create mode 100644 pkg/setup/setup_test.go diff --git a/main.go b/main.go index 1d259243..f96aa97d 100644 --- a/main.go +++ b/main.go @@ -3,7 +3,6 @@ package main import ( "context" "flag" - "io/ioutil" "os" "os/signal" "syscall" @@ -12,9 +11,9 @@ import ( "github.com/resmoio/kubernetes-event-exporter/pkg/exporter" "github.com/resmoio/kubernetes-event-exporter/pkg/kube" "github.com/resmoio/kubernetes-event-exporter/pkg/metrics" + "github.com/resmoio/kubernetes-event-exporter/pkg/setup" "github.com/rs/zerolog" "github.com/rs/zerolog/log" - "gopkg.in/yaml.v2" ) var ( @@ -24,28 +23,29 @@ var ( func main() { flag.Parse() - b, err := ioutil.ReadFile(*conf) + log.Info().Msg("Reading config file " + *conf) + configBytes, err := os.ReadFile(*conf) if err != nil { log.Fatal().Err(err).Msg("cannot read config file") } - b = []byte(os.ExpandEnv(string(b))) + configBytes = []byte(os.ExpandEnv(string(configBytes))) - var cfg exporter.Config - err = yaml.Unmarshal(b, &cfg) + cfg, err := setup.ParseConfigFromBites(configBytes) if err != nil { - log.Fatal().Err(err).Msg("cannot parse config to YAML") + log.Fatal().Msg(err.Error()) } - log.Logger = log.With().Caller().Logger().Level(zerolog.DebugLevel) - if cfg.LogLevel != "" { level, err := zerolog.ParseLevel(cfg.LogLevel) if err != nil { log.Fatal().Err(err).Str("level", cfg.LogLevel).Msg("Invalid log level") } log.Logger = log.Logger.Level(level) + } else { + log.Info().Msg("Set default log level to info. Use config.logLevel=[debug | info | warn | error] to overwrite.") + log.Logger = log.With().Caller().Logger().Level(zerolog.InfoLevel) } if cfg.LogFormat == "json" { diff --git a/pkg/setup/setup.go b/pkg/setup/setup.go new file mode 100644 index 00000000..99ff5dee --- /dev/null +++ b/pkg/setup/setup.go @@ -0,0 +1,33 @@ +package setup + +import ( + "errors" + "strings" + + "github.com/goccy/go-yaml" + "github.com/resmoio/kubernetes-event-exporter/pkg/exporter" +) + +func ParseConfigFromBites(configBytes []byte) (exporter.Config, error) { + var config exporter.Config + err := yaml.Unmarshal(configBytes, &config) + if err != nil { + errMsg := err.Error() + errLines := strings.Split(errMsg, "\n") + if len(errLines) > 0 { + errMsg = errLines[0] + } + for _, line := range errLines { + if strings.Contains(line, "> ") { + errMsg += ": [ line " + line + "]" + if strings.Contains(line, "{{") { + errMsg += ": " + "Need to wrap values with special characters in quotes" + } + } + } + errMsg = "Cannot parse config to YAML: " + errMsg + return exporter.Config{}, errors.New(errMsg) + } + + return config, nil +} diff --git a/pkg/setup/setup_test.go b/pkg/setup/setup_test.go new file mode 100644 index 00000000..e716459d --- /dev/null +++ b/pkg/setup/setup_test.go @@ -0,0 +1,87 @@ +package setup + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_ParseConfigFromBites_ExampleConfigIsCorrect(t *testing.T) { + configBytes, err := os.ReadFile("../../config.example.yaml") + if err != nil { + assert.NoError(t, err, "cannot read config file: "+err.Error()) + return + } + + config, err := ParseConfigFromBites(configBytes) + + assert.NoError(t, err) + assert.NotEmpty(t, config.LogLevel) + assert.NotEmpty(t, config.LogFormat) + assert.NotEmpty(t, config.Route) + assert.NotEmpty(t, config.Route.Routes) + assert.Equal(t, 3, len(config.Route.Routes)) + assert.NotEmpty(t, config.Receivers) + assert.Equal(t, 9, len(config.Receivers)) +} + +func Test_ParseConfigFromBites_NoErrors(t *testing.T) { + configBytes := []byte(` +logLevel: info +logFormat: json +`) + + config, err := ParseConfigFromBites(configBytes) + + assert.NoError(t, err) + assert.Equal(t, "info", config.LogLevel) + assert.Equal(t, "json", config.LogFormat) +} + +func Test_ParseConfigFromBites_ErrorWhenCurlyBracesNotEscaped(t *testing.T) { + configBytes := []byte(` +logLevel: {{info}} +logFormat: json +`) + + config, err := ParseConfigFromBites(configBytes) + + expectedErrorLine := "> 2 | logLevel: {{info}}" + expectedErrorSuggestion := "Need to wrap values with special characters in quotes" + assert.NotNil(t, err) + assert.Contains(t, err.Error(), expectedErrorLine) + assert.Contains(t, err.Error(), expectedErrorSuggestion) + assert.Equal(t, "", config.LogLevel) + assert.Equal(t, "", config.LogFormat) +} + +func Test_ParseConfigFromBites_OkWhenCurlyBracesEscaped(t *testing.T) { + configBytes := []byte(` +logLevel: "{{info}}" +logFormat: json +`) + + config, err := ParseConfigFromBites(configBytes) + + assert.Nil(t, err) + assert.Equal(t, "{{info}}", config.LogLevel) + assert.Equal(t, "json", config.LogFormat) +} + +func Test_ParseConfigFromBites_ErrorErrorNotWithCurlyBraces(t *testing.T) { + configBytes := []byte(` +logLevelNotYAMLErrorError +logFormat: json +`) + + config, err := ParseConfigFromBites(configBytes) + + expectedErrorLine := "> 2 | logLevelNotYAMLErrorError" + expectedErrorSuggestion := "Need to wrap values with special characters in quotes" + assert.NotNil(t, err) + assert.Contains(t, err.Error(), expectedErrorLine) + assert.NotContains(t, err.Error(), expectedErrorSuggestion) + assert.Equal(t, "", config.LogLevel) + assert.Equal(t, "", config.LogFormat) +}