You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
boundary/internal/observability/event/hclog_formatter_node.go

184 lines
5.0 KiB

package event
import (
"bytes"
"context"
"errors"
"fmt"
"reflect"
"github.com/fatih/structs"
"github.com/hashicorp/eventlogger"
"github.com/hashicorp/go-hclog"
)
const (
infoField = "Info"
errorFields = "ErrorFields"
requestInfoField = "RequestInfo"
wrappedField = "Wrapped"
hclogNodeName = "hclog-formatter-filter"
)
// hclogFormatterFilter will format a boundary event an an hclog entry.
type hclogFormatterFilter struct {
// jsonFormat allows you to specify that the hclog entry should be in JSON
// fmt.
jsonFormat bool
predicate func(ctx context.Context, i interface{}) (bool, error)
allow []*filter
deny []*filter
}
func newHclogFormatterFilter(jsonFormat bool, opt ...Option) (*hclogFormatterFilter, error) {
const op = "event.NewHclogFormatter"
n := hclogFormatterFilter{
jsonFormat: jsonFormat,
}
opts := getOpts(opt...)
// intentionally not checking if allow and/or deny optional filters were
// supplied since having a filter node with no filters is okay.
if len(opts.withAllow) > 0 {
n.allow = make([]*filter, 0, len((opts.withAllow)))
for i := range opts.withAllow {
f, err := newFilter(opts.withAllow[i])
if err != nil {
return nil, fmt.Errorf("%s: invalid allow filter '%s': %w", op, opts.withAllow[i], err)
}
n.allow = append(n.allow, f)
}
}
if len(opts.withDeny) > 0 {
n.deny = make([]*filter, 0, len((opts.withDeny)))
for i := range opts.withDeny {
f, err := newFilter(opts.withDeny[i])
if err != nil {
return nil, fmt.Errorf("%s: invalid deny filter '%s': %w", op, opts.withDeny[i], err)
}
n.deny = append(n.deny, f)
}
}
n.predicate = newPredicate(n.allow, n.deny)
return &n, nil
}
// Reopen is a no op
func (_ *hclogFormatterFilter) Reopen() error { return nil }
// Type describes the type of the node as a Formatter.
func (_ *hclogFormatterFilter) Type() eventlogger.NodeType {
return eventlogger.NodeTypeFormatterFilter
}
// Name returns a representation of the HclogFormatter's name
func (_ *hclogFormatterFilter) Name() string {
return hclogNodeName
}
// Process formats the Boundary event as an hclog entry and stores that
// formatted data in Event.Formatted with a key of either "hclog-text"
// (TextHclogSinkFormat) or "hclog-json" (JSONHclogSinkFormat) based on the
// HclogFormatter.JSONFormat value.
//
// If the node has a Predicate, then the filter will be applied to event.Payload
func (f *hclogFormatterFilter) Process(ctx context.Context, e *eventlogger.Event) (*eventlogger.Event, error) {
const op = "event.(HclogFormatter).Process"
if e == nil {
return nil, errors.New("event is nil")
}
if f.predicate != nil {
// Use the predicate to see if we want to keep the event using it's
// formatted struct as a parmeter to the predicate.
keep, err := f.predicate(ctx, e.Payload)
if err != nil {
return nil, fmt.Errorf("%s: unable to filter: %w", op, err)
}
if !keep {
// Return nil to signal that the event should be discarded.
return nil, nil
}
}
var m map[string]interface{}
switch string(e.Type) {
case string(ErrorType), string(AuditType), string(SystemType):
m = structs.Map(e.Payload)
case string(ObservationType):
m = e.Payload.(map[string]interface{})
default:
return nil, fmt.Errorf("%s: unknown event type %s", op, e.Type)
}
args := make([]interface{}, 0, len(m))
for k, v := range m {
if k == requestInfoField && v == nil {
continue
}
if !f.jsonFormat && v != nil {
var underlyingPtr bool
valueKind := reflect.TypeOf(v).Kind()
if valueKind == reflect.Ptr {
underlyingPtr = true
valueKind = reflect.TypeOf(v).Elem().Kind()
}
switch {
case valueKind == reflect.Map:
for sk, sv := range v.(map[string]interface{}) {
args = append(args, k+":"+sk, sv)
}
continue
case valueKind == reflect.Struct:
if underlyingPtr && (v == nil || reflect.ValueOf(v).IsNil()) {
continue
}
for sk, sv := range structs.Map(v) {
args = append(args, k+":"+sk, sv)
}
continue
}
}
switch string(e.Type) {
case string(ErrorType):
switch {
case k == errorFields && v == nil:
continue
case k == infoField && len(v.(map[string]interface{})) == 0:
continue
case k == wrappedField && v == nil:
continue
}
}
args = append(args, k, v)
}
var buf bytes.Buffer
logger := hclog.New(&hclog.LoggerOptions{
Output: &buf,
Level: hclog.Trace,
JSONFormat: f.jsonFormat,
})
const eventMarker = " event"
switch string(e.Type) {
case string(ErrorType):
logger.Error(string(e.Type)+eventMarker, args...)
case string(ObservationType), string(SystemType), string(AuditType):
logger.Info(string(e.Type)+eventMarker, args...)
default:
// well, we should ever hit this, since we should be specific about the
// event type we're processing, but adding this default to just be sure
// we haven't missed anything.
logger.Trace(string(e.Type)+eventMarker, args...)
}
switch f.jsonFormat {
case true:
e.FormattedAs(string(JSONHclogSinkFormat), buf.Bytes())
case false:
e.FormattedAs(string(TextHclogSinkFormat), buf.Bytes())
}
return e, nil
}