VAULT-22482: New audit metrics (#24704)

* VAULT-22481: Audit filter node (#24465)

* Initial commit on adding filter nodes for audit

* tests for audit filter

* test: longer filter - more conditions

* copywrite headers

* Check interface for the right type

* Add audit filtering feature (#24554)

* Support filter nodes in backend factories and add some tests

* More tests and cleanup

* Attempt to move control of registration for nodes and pipelines to the audit broker (#24505)

* invert control of the pipelines/nodes to the audit broker vs. within each backend

* update noop audit test code to implement the pipeliner interface

* noop mount path has trailing slash

* attempting to make NoopAudit more friendly

* NoopAudit uses known salt

* Refactor audit.ProcessManual to support filter nodes

* HasFiltering

* rename the pipeliner

* use exported AuditEvent in Filter

* Add tests for registering and deregistering backends on the audit broker

* Add missing licence header to one file, fix a typo in two tests

---------

Co-authored-by: Peter Wilson <peter.wilson@hashicorp.com>

* Add changelog file

* initial work on global metrics for sink success/failure

* initial work to add a fallback device for audit

* Return when we have outright errors

* Improve comment

* Remove unneeded options on NewBroker and remove the policy opts elsewhere

* Remove duplicate node registration code

* Add more tests for audit backends

* ensure we return the multierror as soon as possible, and append it correctly

* error tweaks for audit: log req/resp

* extract the registration for fallback/normal devices, and ensure we always add to backends when successful

* slightly nicer error message rather than returning the raw err

* refactor the deregister methods for audit broker

* Prevent issues if fallback device is the first device added

* Bail early when the user tries adding more than one fallback audit device

* Check if there is an existing fallback audit device when setting the required sinks threshold for an audit broker

* Use the right ParseBool in audit backends

* Tweak the way we check for the threshold to make it clear why we ignore fallback

* Ensure all 'fallback' settings look the same

* nicer formatting of error

* broker tests for Register

* Deregister tests

* Deregister checks if registered before attempting

* Comment improvement

* Multiple Deregister calls are OK

* Fallback not required in this test

* Sanitise input for Deregister

* Locking mixup

* fix test

* Add changelog

* Check fallback broker's sink success threshold for register/deregister

* Remove changelog

* updated

* better name for the audit metrics labelers

* extra test

* remove name from metric counter type

* update func calls for NewMetricsCounter

* labelers should be pointers to the instance

* revert audit_test complaints about the header

* use constant value for the metric label on a fallback miss

* remove vault prefix from metric labels

* US spelling for labeler and adjust the way the labels are returned

* Fixed name and type we're testing for

* Defensive addition to HasFiltering (no nodemap no filter node)

* Remove dupe code block

* Revert to using armon/go-metrics

* Fallback miss fix

* PR feedback updates

* consistent format for configure methods

* Updated telemetry set up based on PR feedback

---------

Co-authored-by: Kuba Wieczorek <kuba.wieczorek@hashicorp.com>
This commit is contained in:
Peter Wilson
2024-01-10 17:48:06 +00:00
committed by GitHub
parent f130ebaeae
commit 69c1e91679
15 changed files with 553 additions and 61 deletions

View File

@@ -0,0 +1,79 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package event
import (
"context"
"fmt"
"reflect"
"strings"
"github.com/armon/go-metrics"
"github.com/hashicorp/eventlogger"
)
var _ eventlogger.Node = (*MetricsCounter)(nil)
// MetricsCounter offers a way for nodes to emit metrics which increment a label by 1.
type MetricsCounter struct {
Name string
Node eventlogger.Node
labeler Labeler
}
// Labeler provides a way to inject the logic required to determine labels based
// on the state of the eventlogger.Event being returned and the error resulting
// from processing the by the underlying eventlogger.Node.
type Labeler interface {
Labels(*eventlogger.Event, error) []string
}
// NewMetricsCounter should be used to create the MetricsCounter.
func NewMetricsCounter(name string, node eventlogger.Node, labeler Labeler) (*MetricsCounter, error) {
const op = "event.NewMetricsCounter"
name = strings.TrimSpace(name)
if name == "" {
return nil, fmt.Errorf("%s: name is required: %w", op, ErrInvalidParameter)
}
if node == nil || reflect.ValueOf(node).IsNil() {
return nil, fmt.Errorf("%s: node is required: %w", op, ErrInvalidParameter)
}
if labeler == nil || reflect.ValueOf(labeler).IsNil() {
return nil, fmt.Errorf("%s: labeler is required: %w", op, ErrInvalidParameter)
}
return &MetricsCounter{
Name: name,
Node: node,
labeler: labeler,
}, nil
}
// Process will process the event using the underlying eventlogger.Node, and then
// use the configured Labeler to provide a label which is used to increment a metric by 1.
func (m MetricsCounter) Process(ctx context.Context, e *eventlogger.Event) (*eventlogger.Event, error) {
// NOTE: We don't provide an 'op' here, as we're just wrapping the underlying node.
var err error
// Process the node first
e, err = m.Node.Process(ctx, e)
// Provide the results to the Labeler.
metrics.IncrCounter(m.labeler.Labels(e, err), 1)
return e, err
}
// Reopen attempts to reopen the underlying eventlogger.Node.
func (m MetricsCounter) Reopen() error {
return m.Node.Reopen()
}
// Type returns the type for the underlying eventlogger.Node.
func (m MetricsCounter) Type() eventlogger.NodeType {
return m.Node.Type()
}

View File

@@ -0,0 +1,97 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package event
import (
"context"
"testing"
"github.com/hashicorp/eventlogger"
"github.com/stretchr/testify/require"
)
var (
_ eventlogger.Node = (*testEventLoggerNode)(nil)
_ Labeler = (*testMetricsCounter)(nil)
)
// TestNewMetricsCounter ensures that NewMetricsCounter operates as intended and
// can validate the input parameters correctly, returning the right error message
// when required.
func TestNewMetricsCounter(t *testing.T) {
t.Parallel()
tests := map[string]struct {
name string
node eventlogger.Node
labeler Labeler
isErrorExpected bool
expectedErrorMessage string
}{
"happy": {
name: "foo",
node: &testEventLoggerNode{},
labeler: &testMetricsCounter{},
isErrorExpected: false,
},
"no-name": {
node: nil,
labeler: nil,
isErrorExpected: true,
expectedErrorMessage: "event.NewMetricsCounter: name is required: invalid parameter",
},
"no-node": {
name: "foo",
node: nil,
isErrorExpected: true,
expectedErrorMessage: "event.NewMetricsCounter: node is required: invalid parameter",
},
"no-labeler": {
name: "foo",
node: &testEventLoggerNode{},
labeler: nil,
isErrorExpected: true,
expectedErrorMessage: "event.NewMetricsCounter: labeler is required: invalid parameter",
},
}
for name, tc := range tests {
name := name
tc := tc
t.Run(name, func(t *testing.T) {
m, err := NewMetricsCounter(tc.name, tc.node, tc.labeler)
switch {
case tc.isErrorExpected:
require.Error(t, err)
require.EqualError(t, err, tc.expectedErrorMessage)
default:
require.NoError(t, err)
require.NotNil(t, m)
}
})
}
}
// testEventLoggerNode is for testing and implements the eventlogger.Node interface.
type testEventLoggerNode struct{}
func (t testEventLoggerNode) Process(ctx context.Context, e *eventlogger.Event) (*eventlogger.Event, error) {
return nil, nil
}
func (t testEventLoggerNode) Reopen() error {
return nil
}
func (t testEventLoggerNode) Type() eventlogger.NodeType {
return eventlogger.NodeTypeSink
}
// testMetricsCounter is for testing and implements the event.Labeler interface.
type testMetricsCounter struct{}
func (m *testMetricsCounter) Labels(_ *eventlogger.Event, err error) []string {
return []string{""}
}