Audit: optional logger for sinks will log on errors when context is done (#27859)

* Added optional logger for sink nodes (supplied by backends) will log on errors when context is also done

* changelog
This commit is contained in:
Peter Wilson
2024-07-24 22:57:15 +01:00
committed by GitHub
parent 46d2f41000
commit 69c0433f9f
10 changed files with 108 additions and 12 deletions

View File

@@ -76,12 +76,12 @@ func newFileBackend(conf *BackendConfig, headersConfig HeaderFormatter) (*FileBa
return nil, err return nil, err
} }
var opt []event.Option sinkOpts := []event.Option{event.WithLogger(conf.Logger)}
if mode, ok := conf.Config[optionMode]; ok { if mode, ok := conf.Config[optionMode]; ok {
opt = append(opt, event.WithFileMode(mode)) sinkOpts = append(sinkOpts, event.WithFileMode(mode))
} }
err = b.configureSinkNode(conf.MountPath, filePath, cfg.requiredFormat, opt...) err = b.configureSinkNode(conf.MountPath, filePath, cfg.requiredFormat, sinkOpts...)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -70,6 +70,7 @@ func newSocketBackend(conf *BackendConfig, headersConfig HeaderFormatter) (*Sock
sinkOpts := []event.Option{ sinkOpts := []event.Option{
event.WithSocketType(socketType), event.WithSocketType(socketType),
event.WithMaxDuration(writeDeadline), event.WithMaxDuration(writeDeadline),
event.WithLogger(conf.Logger),
} }
err = event.ValidateOptions(sinkOpts...) err = event.ValidateOptions(sinkOpts...)

View File

@@ -60,6 +60,7 @@ func newSyslogBackend(conf *BackendConfig, headersConfig HeaderFormatter) (*Sysl
sinkOpts := []event.Option{ sinkOpts := []event.Option{
event.WithFacility(facility), event.WithFacility(facility),
event.WithTag(tag), event.WithTag(tag),
event.WithLogger(conf.Logger),
} }
err = event.ValidateOptions(sinkOpts...) err = event.ValidateOptions(sinkOpts...)

4
changelog/27859.txt Normal file
View File

@@ -0,0 +1,4 @@
```release-note:improvement
audit: sinks (file, socket, syslog) will attempt to log errors to the server operational
log before returning (if there are errors to log, and the context is done).
```

View File

@@ -6,10 +6,12 @@ package event
import ( import (
"fmt" "fmt"
"os" "os"
"reflect"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-secure-stdlib/parseutil" "github.com/hashicorp/go-secure-stdlib/parseutil"
"github.com/hashicorp/go-uuid" "github.com/hashicorp/go-uuid"
) )
@@ -26,6 +28,7 @@ type options struct {
withSocketType string withSocketType string
withMaxDuration time.Duration withMaxDuration time.Duration
withFileMode *os.FileMode withFileMode *os.FileMode
withLogger hclog.Logger
} }
// getDefaultOptions returns Options with their default values. // getDefaultOptions returns Options with their default values.
@@ -201,3 +204,15 @@ func WithFileMode(mode string) Option {
return nil return nil
} }
} }
// WithLogger provides an Option to supply a logger which will be used to write logs.
// NOTE: If no logger is supplied then logging may not be possible.
func WithLogger(l hclog.Logger) Option {
return func(o *options) error {
if l != nil && !reflect.ValueOf(l).IsNil() {
o.withLogger = l
}
return nil
}
}

View File

@@ -8,6 +8,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/hashicorp/go-hclog"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@@ -423,3 +424,37 @@ func TestOptions_WithFileMode(t *testing.T) {
}) })
} }
} }
// TestOptions_WithLogger exercises WithLogger Option to ensure it performs as expected.
func TestOptions_WithLogger(t *testing.T) {
t.Parallel()
tests := map[string]struct {
value hclog.Logger
isNilExpected bool
}{
"nil-pointer": {
value: nil,
isNilExpected: true,
},
"logger": {
value: hclog.NewNullLogger(),
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
t.Parallel()
opts := &options{}
applyOption := WithLogger(tc.value)
err := applyOption(opts)
require.NoError(t, err)
if tc.isNilExpected {
require.Nil(t, opts.withLogger)
} else {
require.NotNil(t, opts.withLogger)
}
})
}
}

View File

@@ -14,6 +14,7 @@ import (
"sync" "sync"
"github.com/hashicorp/eventlogger" "github.com/hashicorp/eventlogger"
"github.com/hashicorp/go-hclog"
) )
// defaultFileMode is the default file permissions (read/write for everyone). // defaultFileMode is the default file permissions (read/write for everyone).
@@ -31,6 +32,7 @@ type FileSink struct {
fileMode os.FileMode fileMode os.FileMode
path string path string
requiredFormat string requiredFormat string
logger hclog.Logger
} }
// NewFileSink should be used to create a new FileSink. // NewFileSink should be used to create a new FileSink.
@@ -69,6 +71,7 @@ func NewFileSink(path string, format string, opt ...Option) (*FileSink, error) {
fileMode: mode, fileMode: mode,
requiredFormat: format, requiredFormat: format,
path: p, path: p,
logger: opts.withLogger,
} }
// Ensure that the file can be successfully opened for writing; // Ensure that the file can be successfully opened for writing;
@@ -82,13 +85,22 @@ func NewFileSink(path string, format string, opt ...Option) (*FileSink, error) {
} }
// Process handles writing the event to the file sink. // Process handles writing the event to the file sink.
func (s *FileSink) Process(ctx context.Context, e *eventlogger.Event) (*eventlogger.Event, error) { func (s *FileSink) Process(ctx context.Context, e *eventlogger.Event) (_ *eventlogger.Event, retErr error) {
select { select {
case <-ctx.Done(): case <-ctx.Done():
return nil, ctx.Err() return nil, ctx.Err()
default: default:
} }
defer func() {
// If the context is errored (cancelled), and we were planning to return
// an error, let's also log (if we have a logger) in case the eventlogger's
// status channel and errors propagated.
if err := ctx.Err(); err != nil && retErr != nil && s.logger != nil {
s.logger.Error("file sink error", "context", err, "error", retErr)
}
}()
if e == nil { if e == nil {
return nil, fmt.Errorf("event is nil: %w", ErrInvalidParameter) return nil, fmt.Errorf("event is nil: %w", ErrInvalidParameter)
} }

View File

@@ -12,6 +12,7 @@ import (
"time" "time"
"github.com/hashicorp/eventlogger" "github.com/hashicorp/eventlogger"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-multierror" "github.com/hashicorp/go-multierror"
) )
@@ -25,6 +26,7 @@ type SocketSink struct {
maxDuration time.Duration maxDuration time.Duration
socketLock sync.RWMutex socketLock sync.RWMutex
connection net.Conn connection net.Conn
logger hclog.Logger
} }
// NewSocketSink should be used to create a new SocketSink. // NewSocketSink should be used to create a new SocketSink.
@@ -52,21 +54,28 @@ func NewSocketSink(address string, format string, opt ...Option) (*SocketSink, e
maxDuration: opts.withMaxDuration, maxDuration: opts.withMaxDuration,
socketLock: sync.RWMutex{}, socketLock: sync.RWMutex{},
connection: nil, connection: nil,
logger: opts.withLogger,
} }
return sink, nil return sink, nil
} }
// Process handles writing the event to the socket. // Process handles writing the event to the socket.
func (s *SocketSink) Process(ctx context.Context, e *eventlogger.Event) (*eventlogger.Event, error) { func (s *SocketSink) Process(ctx context.Context, e *eventlogger.Event) (_ *eventlogger.Event, retErr error) {
select { select {
case <-ctx.Done(): case <-ctx.Done():
return nil, ctx.Err() return nil, ctx.Err()
default: default:
} }
s.socketLock.Lock() defer func() {
defer s.socketLock.Unlock() // If the context is errored (cancelled), and we were planning to return
// an error, let's also log (if we have a logger) in case the eventlogger's
// status channel and errors propagated.
if err := ctx.Err(); err != nil && retErr != nil && s.logger != nil {
s.logger.Error("socket sink error", "context", err, "error", retErr)
}
}()
if e == nil { if e == nil {
return nil, fmt.Errorf("event is nil: %w", ErrInvalidParameter) return nil, fmt.Errorf("event is nil: %w", ErrInvalidParameter)
@@ -77,6 +86,9 @@ func (s *SocketSink) Process(ctx context.Context, e *eventlogger.Event) (*eventl
return nil, fmt.Errorf("unable to retrieve event formatted as %q: %w", s.requiredFormat, ErrInvalidParameter) return nil, fmt.Errorf("unable to retrieve event formatted as %q: %w", s.requiredFormat, ErrInvalidParameter)
} }
s.socketLock.Lock()
defer s.socketLock.Unlock()
// Try writing and return early if successful. // Try writing and return early if successful.
err := s.write(ctx, formatted) err := s.write(ctx, formatted)
if err == nil { if err == nil {

View File

@@ -9,6 +9,7 @@ import (
"strings" "strings"
"github.com/hashicorp/eventlogger" "github.com/hashicorp/eventlogger"
"github.com/hashicorp/go-hclog"
gsyslog "github.com/hashicorp/go-syslog" gsyslog "github.com/hashicorp/go-syslog"
) )
@@ -17,7 +18,8 @@ var _ eventlogger.Node = (*SyslogSink)(nil)
// SyslogSink is a sink node which handles writing events to syslog. // SyslogSink is a sink node which handles writing events to syslog.
type SyslogSink struct { type SyslogSink struct {
requiredFormat string requiredFormat string
logger gsyslog.Syslogger syslogger gsyslog.Syslogger
logger hclog.Logger
} }
// NewSyslogSink should be used to create a new SyslogSink. // NewSyslogSink should be used to create a new SyslogSink.
@@ -38,17 +40,32 @@ func NewSyslogSink(format string, opt ...Option) (*SyslogSink, error) {
return nil, fmt.Errorf("error creating syslogger: %w", err) return nil, fmt.Errorf("error creating syslogger: %w", err)
} }
return &SyslogSink{requiredFormat: format, logger: logger}, nil syslog := &SyslogSink{
requiredFormat: format,
syslogger: logger,
logger: opts.withLogger,
}
return syslog, nil
} }
// Process handles writing the event to the syslog. // Process handles writing the event to the syslog.
func (s *SyslogSink) Process(ctx context.Context, e *eventlogger.Event) (*eventlogger.Event, error) { func (s *SyslogSink) Process(ctx context.Context, e *eventlogger.Event) (_ *eventlogger.Event, retErr error) {
select { select {
case <-ctx.Done(): case <-ctx.Done():
return nil, ctx.Err() return nil, ctx.Err()
default: default:
} }
defer func() {
// If the context is errored (cancelled), and we were planning to return
// an error, let's also log (if we have a logger) in case the eventlogger's
// status channel and errors propagated.
if err := ctx.Err(); err != nil && retErr != nil && s.logger != nil {
s.logger.Error("syslog sink error", "context", err, "error", retErr)
}
}()
if e == nil { if e == nil {
return nil, fmt.Errorf("event is nil: %w", ErrInvalidParameter) return nil, fmt.Errorf("event is nil: %w", ErrInvalidParameter)
} }
@@ -58,7 +75,7 @@ func (s *SyslogSink) Process(ctx context.Context, e *eventlogger.Event) (*eventl
return nil, fmt.Errorf("unable to retrieve event formatted as %q: %w", s.requiredFormat, ErrInvalidParameter) return nil, fmt.Errorf("unable to retrieve event formatted as %q: %w", s.requiredFormat, ErrInvalidParameter)
} }
_, err := s.logger.Write(formatted) _, err := s.syslogger.Write(formatted)
if err != nil { if err != nil {
return nil, fmt.Errorf("error writing to syslog: %w", err) return nil, fmt.Errorf("error writing to syslog: %w", err)
} }

View File

@@ -160,7 +160,6 @@ func (c *Core) enableAudit(ctx context.Context, entry *MountEntry, updateStorage
if err != nil { if err != nil {
c.logger.Error("new audit backend failed test", "path", entry.Path, "type", entry.Type, "error", err) c.logger.Error("new audit backend failed test", "path", entry.Path, "type", entry.Type, "error", err)
return fmt.Errorf("audit backend failed test message: %w", err) return fmt.Errorf("audit backend failed test message: %w", err)
} }
} }