From ad37074b43abf7b9f3b28eb0eb2b3cbb5bd63b29 Mon Sep 17 00:00:00 2001 From: Owen Cabalceta Date: Thu, 10 Oct 2024 14:13:17 -0400 Subject: [PATCH] feat: new wrp validator, add the ability to validate whether wrp messages' source/dest are not empty - new wrp validators: none_empty_destination & none_empty_source - validates whether wrp messages' source/dest are not empty - includes metrics for the new validator other things included: - fix doc typos - improve metric help docs --- wrpvalidator/metaValidator.go | 10 ++++ wrpvalidator/metaValidator_test.go | 18 ++++++ wrpvalidator/metrics.go | 62 +++++++++++++++----- wrpvalidator/specValidator.go | 71 +++++++++++++++++++++-- wrpvalidator/specValidator_test.go | 92 ++++++++++++++++++++++++++++++ wrpvalidator/validatorType.go | 10 ++++ wrpvalidator/validatorType_test.go | 18 ++++++ 7 files changed, 260 insertions(+), 21 deletions(-) diff --git a/wrpvalidator/metaValidator.go b/wrpvalidator/metaValidator.go index 5838a5b..4572a8f 100644 --- a/wrpvalidator/metaValidator.go +++ b/wrpvalidator/metaValidator.go @@ -83,10 +83,16 @@ func (v *MetaValidator) UnmarshalJSON(b []byte) error { val = SimpleEventType case SpansType: val = Spans + case NoneEmptySourceType: + val = NoneEmptySource + case NoneEmptyDestinationType: + val = NoneEmptyDestination default: return fmt.Errorf("validator `%s`: wrp validator selection: %w: %s", v.meta.Type, ErrValidatorUnmarshalling, errValidatorTypeInvalid) } + // By default validators will have no metrics. + // Metrics are optional and can be added with `MetaValidator.AddMetric()`. v.validator = NewValidatorWithoutMetric(val) if !v.IsValid() { @@ -128,6 +134,10 @@ func (v *MetaValidator) AddMetric(tf *touchstone.Factory, labelNames ...string) val, err = NewSimpleEventTypeWithMetric(tf, labelNames...) case SpansType: val, err = NewSpansWithMetric(tf, labelNames...) + case NoneEmptySourceType: + val, err = NewNoneEmptySourceWithMetric(tf, labelNames...) + case NoneEmptyDestinationType: + val, err = NewNoneEmptyDestinationWithMetric(tf, labelNames...) // no default is needed since v.IsValid() takes care of this case } diff --git a/wrpvalidator/metaValidator_test.go b/wrpvalidator/metaValidator_test.go index d0bb4c9..da48d4e 100644 --- a/wrpvalidator/metaValidator_test.go +++ b/wrpvalidator/metaValidator_test.go @@ -266,6 +266,24 @@ func TestMetaValidatorAddMetric(t *testing.T) { } ]`), }, + { + description: "Add metric validator NoneEmptySource", + config: []byte(`[ + { + "type": "none_empty_source", + "level": "warning" + } + ]`), + }, + { + description: "Add metric validator NoneEmptyDestinationType", + config: []byte(`[ + { + "type": "none_empty_destination", + "level": "warning" + } + ]`), + }, { description: "Add metric validator always_invalid", config: []byte(`[ diff --git a/wrpvalidator/metrics.go b/wrpvalidator/metrics.go index 5b94b07..65801a0 100644 --- a/wrpvalidator/metrics.go +++ b/wrpvalidator/metrics.go @@ -12,53 +12,65 @@ const ( // MetricPrefix is prepended to all metrics exposed by this package. metricPrefix = "wrp_validator_" - // alwaysInvalidValidatorErrorTotalName is the name of the counter for all AlwaysInvalid validation. + // alwaysInvalidValidatorErrorTotalName is the name of the counter for all AlwaysInvalid validation errors. alwaysInvalidValidatorErrorTotalName = metricPrefix + "always_invalid" // alwaysInvalidValidatorErrorTotalHelp is the help text for the AlwaysInvalid metric. alwaysInvalidValidatorErrorTotalHelp = "the total number of AlwaysInvalid validations" - // destinationValidatorErrorTotalName is the name of the counter for all destination validation. + // destinationValidatorErrorTotalName is the name of the counter for all destination validation errors. destinationValidatorErrorTotalName = metricPrefix + "destination" // destinationValidatorErrorTotalHelp is the help text for the Destination metric. - destinationValidatorErrorTotalHelp = "the total number of Destination metric" + destinationValidatorErrorTotalHelp = "the total number of Destination validation errors" - // sourceValidatorErrorTotalName is the name of the counter for all source validation. + // sourceValidatorErrorTotalName is the name of the counter for all source validation errors. sourceValidatorErrorTotalName = metricPrefix + "source" // sourceValidatorErrorTotalHelp is the help text for the Source metric. - sourceValidatorErrorTotalHelp = "the total number of Source metric" + sourceValidatorErrorTotalHelp = "the total number of Source validation errors" - // messageTypeValidatorErrorTotalName is the name of the counter for all MessageType validation. + // messageTypeValidatorErrorTotalName is the name of the counter for all MessageType validation errors. messageTypeValidatorErrorTotalName = metricPrefix + "message_type" // messageTypeValidatorErrorTotalHelp is the help text for the MessageType metric. - messageTypeValidatorErrorTotalHelp = "the total number of MessageType metric" + messageTypeValidatorErrorTotalHelp = "the total number of MessageType validation errors" - // utf8ValidatorErrorTotalName is the name of the counter for all UTF8 validation. + // utf8ValidatorErrorTotalName is the name of the counter for all UTF8 validation errors. utf8ValidatorErrorTotalName = metricPrefix + "utf8" // utf8ValidatorErrorTotalHelp is the help text for the UTF8 Validator metric. - utf8ValidatorErrorTotalHelp = "the total number of UTF8 Validator metric" + utf8ValidatorErrorTotalHelp = "the total number of UTF8 validation errors" - // simpleEventTypeValidatorErrorTotalName is the name of the counter for all SimpleEventType validation. + // simpleEventTypeValidatorErrorTotalName is the name of the counter for all SimpleEventType validation errors. simpleEventTypeValidatorErrorTotalName = metricPrefix + "simple_event_type" // simpleEventTypeValidatorErrorTotalHelp is the help text for the SimpleEventType Validator metric. - simpleEventTypeValidatorErrorTotalHelp = "the total number of SimpleEventType Validator metric" + simpleEventTypeValidatorErrorTotalHelp = "the total number of SimpleEventType validation errors" - // simpleRequestResponseMessageTypeErrorTotalName is the name of the counter for all SimpleRequestResponseMessageType validation. + // simpleRequestResponseMessageTypeErrorTotalName is the name of the counter for all SimpleRequestResponseMessageType validation errors. simpleRequestResponseMessageTypeErrorTotalName = metricPrefix + "simple_request_response_message_type" // simpleRequestResponseMessageTypeErrorTotalHelp is the help text for the SimpleRequestResponseMessageType Validator metric. - simpleRequestResponseMessageTypeErrorTotalHelp = "the total number of SimpleRequestResponseMessageType Validator metric" + simpleRequestResponseMessageTypeErrorTotalHelp = "the total number of SimpleRequestResponseMessageType validation errors" - // spansValidatorErrorTotalName is the name of the counter for all Spans validation. + // spansValidatorErrorTotalName is the name of the counter for all Spans validation errors. spansValidatorErrorTotalName = metricPrefix + "spans" // spansValidatorErrorTotalHelp is the help text for the Spans Validator metric. - spansValidatorErrorTotalHelp = "the total number of Spans Validator metric" + spansValidatorErrorTotalHelp = "the total number of Spans validation errors" + + // noneEmptySourceErrorTotalName is the name of the counter for all noneEmptySource validation errors. + noneEmptySourceErrorTotalName = metricPrefix + "none_empty_source" + + // noneEmptySourceErrorTotalHelp is the help text for the noneEmptySource Validator metric. + noneEmptySourceErrorTotalHelp = "the total number of None Empty Source validation errors" + + // noneEmptyDestinationErrorTotalName is the name of the counter for all noneEmptyDestination validation errors. + noneEmptyDestinationErrorTotalName = metricPrefix + "none_empty_destination" + + // noneEmptyDestinationErrorTotalHelp is the help text for the noneEmptyDestination Validator metric. + noneEmptyDestinationErrorTotalHelp = "the total number of None Empty Destination validation errors" ) // Metric label names @@ -147,3 +159,23 @@ func newSpansErrorTotal(tf *touchstone.Factory, labelNames ...string) (m *promet labelNames..., ) } + +func newNoneEmptySourceErrorTotal(tf *touchstone.Factory, labelNames ...string) (m *prometheus.CounterVec, err error) { + return tf.NewCounterVec( + prometheus.CounterOpts{ + Name: noneEmptySourceErrorTotalName, + Help: noneEmptySourceErrorTotalHelp, + }, + labelNames..., + ) +} + +func newNoneEmptyDestinationErrorTotal(tf *touchstone.Factory, labelNames ...string) (m *prometheus.CounterVec, err error) { + return tf.NewCounterVec( + prometheus.CounterOpts{ + Name: noneEmptyDestinationErrorTotalName, + Help: noneEmptyDestinationErrorTotalHelp, + }, + labelNames..., + ) +} diff --git a/wrpvalidator/specValidator.go b/wrpvalidator/specValidator.go index effdb18..c3bafc3 100644 --- a/wrpvalidator/specValidator.go +++ b/wrpvalidator/specValidator.go @@ -19,11 +19,14 @@ const ( ) var ( - ErrorInvalidMessageEncoding = NewValidatorError(errors.New("invalid message encoding"), "", nil) - ErrorInvalidMessageType = NewValidatorError(errors.New("invalid message type"), "", []string{"Type"}) - ErrorInvalidSource = NewValidatorError(errors.New("invalid Source name"), "", []string{"Source"}) - ErrorInvalidDestination = NewValidatorError(errors.New("invalid Destination name"), "", []string{"Destination"}) - errorInvalidUUID = errors.New("invalid UUID") + ErrorInvalidMessageEncoding = NewValidatorError(errors.New("invalid message encoding"), "", nil) + ErrorInvalidMessageType = NewValidatorError(errors.New("invalid message type"), "", []string{"Type"}) + ErrorInvalidSource = NewValidatorError(errors.New("invalid Source name"), "", []string{"Source"}) + ErrorInvalidDestination = NewValidatorError(errors.New("invalid Destination name"), "", []string{"Destination"}) + ErrorInvalidEmptySource = NewValidatorError(errors.New("empty Source"), "", []string{"Source"}) + ErrorInvalidEmptyDestination = NewValidatorError(errors.New("empty Destination"), "", []string{"Destination"}) + + errorInvalidUUID = errors.New("invalid UUID") ) // SpecWithMetrics ensures messages are valid based on each spec validator in the list. @@ -51,7 +54,17 @@ func SpecWithMetrics(tf *touchstone.Factory, labelNames ...string) (Validators, errs = multierr.Append(errs, err) } - return Validators{}.AddFunc(utf8v, mtv, sv, dv), errs + nesv, err := NewNoneEmptySourceWithMetric(tf, labelNames...) + if err != nil { + errs = multierr.Append(errs, err) + } + + nedv, err := NewNoneEmptyDestinationWithMetric(tf, labelNames...) + if err != nil { + errs = multierr.Append(errs, err) + } + + return Validators{}.AddFunc(utf8v, mtv, sv, dv, nesv, nedv), errs } // NewUTF8WithMetric returns a UTF8 validator with a metric middleware. @@ -110,6 +123,34 @@ func NewDestinationWithMetric(tf *touchstone.Factory, labelNames ...string) (Val }, err } +// NewNoneEmptySourceWithMetric returns a NoneEmptySource validator with a metric middleware. +func NewNoneEmptySourceWithMetric(tf *touchstone.Factory, labelNames ...string) (ValidatorFunc, error) { + m, err := newNoneEmptySourceErrorTotal(tf, labelNames...) + + return func(msg wrp.Message, ls prometheus.Labels) error { + err := NoneEmptySource(msg) + if err != nil { + m.With(ls).Add(1.0) + } + + return err + }, err +} + +// NewNoneEmptyDestinationWithMetric returns a NoneEmptyDestination validator with a metric middleware. +func NewNoneEmptyDestinationWithMetric(tf *touchstone.Factory, labelNames ...string) (ValidatorFunc, error) { + m, err := newNoneEmptyDestinationErrorTotal(tf, labelNames...) + + return func(msg wrp.Message, ls prometheus.Labels) error { + err := NoneEmptyDestination(msg) + if err != nil { + m.With(ls).Add(1.0) + } + + return err + }, err +} + // UTF8 takes messages and validates that it contains UTF-8 strings. func UTF8(m wrp.Message) error { if err := wrp.UTF8(m); err != nil { @@ -150,6 +191,24 @@ func Destination(m wrp.Message) error { return nil } +// NoneEmptySource takes messages and validates whether their Source is not empty. +func NoneEmptySource(m wrp.Message) error { + if m.Source == "" { + return ErrorInvalidEmptySource + } + + return nil +} + +// NoneEmptyDestination takes messages and validates whether their Destination is not empty. +func NoneEmptyDestination(m wrp.Message) error { + if m.Destination == "" { + return ErrorInvalidEmptyDestination + } + + return nil +} + // validateLocator validates a given locator's scheme and authority (ID). // Only mac and uuid schemes' IDs are validated. IDs from serial, event and dns schemes are // not validated. diff --git a/wrpvalidator/specValidator_test.go b/wrpvalidator/specValidator_test.go index 453ffb9..ec42722 100644 --- a/wrpvalidator/specValidator_test.go +++ b/wrpvalidator/specValidator_test.go @@ -24,6 +24,8 @@ func TestSpecHelperValidators(t *testing.T) { {"MessageType", testMessageType}, {"Source", testSource}, {"Destination", testDestination}, + {"NoneEmptySource", testNoneEmptySource}, + {"NoneEmptyDestination", testNoneEmptyDestination}, {"validateLocator", testValidateLocator}, } @@ -225,6 +227,24 @@ func TestSpecWithDuplicateValidators(t *testing.T) { require.NoError(err) _, err = SpecWithMetrics(f4) require.Error(err) + + _, pr5, err := touchstone.New(cfg) + require.NoError(err) + + f5 := touchstone.NewFactory(cfg, sallust.Default(), pr5) + _, err = NewNoneEmptySourceWithMetric(f5) + require.NoError(err) + _, err = SpecWithMetrics(f5) + require.Error(err) + + _, pr6, err := touchstone.New(cfg) + require.NoError(err) + + f6 := touchstone.NewFactory(cfg, sallust.Default(), pr6) + _, err = NewNoneEmptyDestinationWithMetric(f6) + require.NoError(err) + _, err = SpecWithMetrics(f6) + require.Error(err) }) } } @@ -579,6 +599,78 @@ func testDestination(t *testing.T) { } } +func testNoneEmptySource(t *testing.T) { + tests := []struct { + description string + msg wrp.Message + expectedErr error + }{ + // Success case + { + description: "Source success", + msg: wrp.Message{Source: "MAC:11:22:33:44:55:66"}, + }, + // Failures + { + description: "NoneEmptySource error", + msg: wrp.Message{Source: ""}, + expectedErr: ErrorInvalidEmptySource, + }, + } + + for _, tc := range tests { + t.Run(tc.description, func(t *testing.T) { + assert := assert.New(t) + err := NoneEmptySource(tc.msg) + if expectedErr := tc.expectedErr; expectedErr != nil { + var targetErr ValidatorError + + assert.ErrorAs(expectedErr, &targetErr) + assert.ErrorIs(err, targetErr.Err) + return + } + + assert.NoError(err) + }) + } +} + +func testNoneEmptyDestination(t *testing.T) { + tests := []struct { + description string + msg wrp.Message + expectedErr error + }{ + // Success case + { + description: "NoneEmptyDestination success", + msg: wrp.Message{Destination: "MAC:11:22:33:44:55:66"}, + }, + // Failures + { + description: "NoneEmptyDestination error", + msg: wrp.Message{Destination: ""}, + expectedErr: ErrorInvalidEmptyDestination, + }, + } + + for _, tc := range tests { + t.Run(tc.description, func(t *testing.T) { + assert := assert.New(t) + err := NoneEmptyDestination(tc.msg) + if expectedErr := tc.expectedErr; expectedErr != nil { + var targetErr ValidatorError + + assert.ErrorAs(expectedErr, &targetErr) + assert.ErrorIs(err, targetErr.Err) + return + } + + assert.NoError(err) + }) + } +} + func testValidateLocator(t *testing.T) { tests := []struct { description string diff --git a/wrpvalidator/validatorType.go b/wrpvalidator/validatorType.go index 5d0f44b..0908ad2 100644 --- a/wrpvalidator/validatorType.go +++ b/wrpvalidator/validatorType.go @@ -23,6 +23,10 @@ const ( SimpleResponseRequestTypeType SimpleEventTypeType SpansType + // Add-ons + NoneEmptySourceType + NoneEmptyDestinationType + // End of list. lastType ) @@ -40,6 +44,9 @@ var ( "simple_res_req": SimpleResponseRequestTypeType, "simple_event": SimpleEventTypeType, "spans": SpansType, + // Add-ons + "none_empty_source": NoneEmptySourceType, + "none_empty_destination": NoneEmptyDestinationType, } validatorTypeMarshal = map[validatorType]string{ UnknownType: "unknown", @@ -52,6 +59,9 @@ var ( SimpleResponseRequestTypeType: "simple_res_req", SimpleEventTypeType: "simple_event", SpansType: "spans", + // Add-ons + NoneEmptySourceType: "none_empty_source", + NoneEmptyDestinationType: "none_empty_destination", } ) diff --git a/wrpvalidator/validatorType_test.go b/wrpvalidator/validatorType_test.go index a0cef55..65f1578 100644 --- a/wrpvalidator/validatorType_test.go +++ b/wrpvalidator/validatorType_test.go @@ -55,6 +55,14 @@ func TestTypeUnmarshalling(t *testing.T) { description: "SpansType valid", config: []byte("spans"), }, + { + description: "NoneEmptySourceType valid", + config: []byte("none_empty_source"), + }, + { + description: "NoneEmptyDestinationType valid", + config: []byte("none_empty_destination"), + }, { description: "Nonexistent type invalid", config: []byte("FOOBAR"), @@ -139,6 +147,16 @@ func TestTypeState(t *testing.T) { val: SpansType, expectedVal: "spans", }, + { + description: "NoneEmptySourceType valid", + val: NoneEmptySourceType, + expectedVal: "none_empty_source", + }, + { + description: "NoneEmptyDestinationType valid", + val: NoneEmptyDestinationType, + expectedVal: "none_empty_destination", + }, { description: "lastLevel valid", val: lastType,