mirror of https://github.com/hashicorp/boundary
support new event formats of: hclog-text and hclog-json (#1440)
parent
e3012afb4f
commit
3a3f956615
@ -0,0 +1,179 @@
|
||||
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 {
|
||||
valueKind := reflect.TypeOf(v).Kind()
|
||||
if valueKind == reflect.Ptr {
|
||||
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 && v != nil && !reflect.ValueOf(v).IsNil():
|
||||
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
|
||||
}
|
||||
@ -0,0 +1,368 @@
|
||||
package event
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/eventlogger"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestHclogFormatter_Process(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
|
||||
f, e := newFilter(`Op == "match-filter"`)
|
||||
require.NoError(t, e)
|
||||
|
||||
testPredicate := newPredicate([]*filter{f}, nil)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
formatter *hclogFormatterFilter
|
||||
e *eventlogger.Event
|
||||
wantErrContains string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "nil event",
|
||||
formatter: &hclogFormatterFilter{
|
||||
jsonFormat: false,
|
||||
},
|
||||
wantErrContains: "event is nil",
|
||||
},
|
||||
{
|
||||
name: "invalid-event-type",
|
||||
formatter: &hclogFormatterFilter{
|
||||
jsonFormat: false,
|
||||
},
|
||||
e: &eventlogger.Event{Type: eventlogger.EventType("invalid-type")},
|
||||
wantErrContains: "unknown event type invalid-type",
|
||||
},
|
||||
{
|
||||
name: "sys-text",
|
||||
formatter: &hclogFormatterFilter{
|
||||
jsonFormat: false,
|
||||
},
|
||||
e: &eventlogger.Event{
|
||||
Type: eventlogger.EventType(SystemType),
|
||||
Payload: &sysEvent{
|
||||
Id: "1",
|
||||
Version: errorVersion,
|
||||
Op: Op("text"),
|
||||
Data: map[string]interface{}{
|
||||
"msg": "hello",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []string{
|
||||
"[INFO] system event:",
|
||||
"Data:msg=hello",
|
||||
"Id=1",
|
||||
"Version=v0.1",
|
||||
"Op=text",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "observation-text",
|
||||
formatter: &hclogFormatterFilter{
|
||||
jsonFormat: false,
|
||||
},
|
||||
e: &eventlogger.Event{
|
||||
Type: eventlogger.EventType(ObservationType),
|
||||
Payload: map[string]interface{}{
|
||||
"id": "1",
|
||||
"version": observationVersion,
|
||||
"latency-ms": 10,
|
||||
},
|
||||
},
|
||||
want: []string{
|
||||
"[INFO] observation event:",
|
||||
"latency-ms=10",
|
||||
"id=1",
|
||||
"version=v0.1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "observation-json",
|
||||
formatter: &hclogFormatterFilter{
|
||||
jsonFormat: true,
|
||||
},
|
||||
e: &eventlogger.Event{
|
||||
Type: eventlogger.EventType(ObservationType),
|
||||
Payload: map[string]interface{}{
|
||||
"id": "1",
|
||||
"version": observationVersion,
|
||||
"latency-ms": 10,
|
||||
},
|
||||
},
|
||||
want: []string{
|
||||
"{\"@level\":\"info\",\"@message\":\"observation event\"",
|
||||
"\"latency-ms\":10",
|
||||
"\"id\":\"1\"",
|
||||
"\"version\":\"v0.1\"}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "err-text",
|
||||
formatter: &hclogFormatterFilter{
|
||||
jsonFormat: false,
|
||||
},
|
||||
e: &eventlogger.Event{
|
||||
Type: eventlogger.EventType(ErrorType),
|
||||
Payload: &err{
|
||||
Id: "1",
|
||||
Version: errorVersion,
|
||||
Error: ErrInvalidParameter.Error(),
|
||||
Op: Op("text"),
|
||||
},
|
||||
},
|
||||
want: []string{
|
||||
"[ERROR] error event:",
|
||||
"Error=\"invalid parameter\"",
|
||||
"Id=1",
|
||||
"Version=v0.1",
|
||||
"Op=text",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "err-json",
|
||||
formatter: &hclogFormatterFilter{
|
||||
jsonFormat: true,
|
||||
},
|
||||
e: &eventlogger.Event{
|
||||
Type: eventlogger.EventType(ErrorType),
|
||||
Payload: &err{
|
||||
Id: "1",
|
||||
Version: errorVersion,
|
||||
Error: ErrInvalidParameter.Error(),
|
||||
Op: Op("text"),
|
||||
},
|
||||
},
|
||||
want: []string{
|
||||
"{\"@level\":\"error\",\"@message\":\"error event\"",
|
||||
"\"Error\":\"invalid parameter\"",
|
||||
"\"Id\":\"1\"",
|
||||
"\"Version\":\"v0.1\"",
|
||||
"\"Op\":\"text\""},
|
||||
},
|
||||
{
|
||||
name: "err-text-with-optional",
|
||||
formatter: &hclogFormatterFilter{
|
||||
jsonFormat: false,
|
||||
},
|
||||
e: &eventlogger.Event{
|
||||
Type: eventlogger.EventType(ErrorType),
|
||||
Payload: &err{
|
||||
Id: "1",
|
||||
Version: errorVersion,
|
||||
Error: ErrInvalidParameter.Error(),
|
||||
Op: Op("text"),
|
||||
Info: map[string]interface{}{"name": "alice"},
|
||||
},
|
||||
},
|
||||
want: []string{
|
||||
"[ERROR] error event:",
|
||||
"Error=\"invalid parameter\"",
|
||||
"Id=1",
|
||||
"Version=v0.1",
|
||||
"Info:name=alice",
|
||||
"Op=text",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "filter-match",
|
||||
formatter: &hclogFormatterFilter{
|
||||
jsonFormat: false,
|
||||
predicate: testPredicate,
|
||||
},
|
||||
e: &eventlogger.Event{
|
||||
Type: eventlogger.EventType(SystemType),
|
||||
Payload: &sysEvent{
|
||||
Id: "1",
|
||||
Version: errorVersion,
|
||||
Op: Op("match-filter"),
|
||||
Data: map[string]interface{}{
|
||||
"msg": "hello",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []string{
|
||||
"[INFO] system event:",
|
||||
"Data:msg=hello",
|
||||
"Id=1",
|
||||
"Version=v0.1",
|
||||
"Op=match-filter",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "filter-no-match",
|
||||
formatter: &hclogFormatterFilter{
|
||||
jsonFormat: false,
|
||||
predicate: testPredicate,
|
||||
},
|
||||
e: &eventlogger.Event{
|
||||
Type: eventlogger.EventType(SystemType),
|
||||
Payload: &sysEvent{
|
||||
Id: "1",
|
||||
Version: errorVersion,
|
||||
Op: Op("doesn't match"),
|
||||
Data: map[string]interface{}{
|
||||
"msg": "hello",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
e, err := tt.formatter.Process(ctx, tt.e)
|
||||
if tt.wantErrContains != "" {
|
||||
require.Error(err)
|
||||
assert.Contains(err.Error(), tt.wantErrContains)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
if len(tt.want) == 0 {
|
||||
assert.Nil(e)
|
||||
return
|
||||
}
|
||||
assert.NotNil(e)
|
||||
var b []byte
|
||||
var ok bool
|
||||
switch tt.formatter.jsonFormat {
|
||||
case true:
|
||||
b, ok = e.Format(string(JSONHclogSinkFormat))
|
||||
case false:
|
||||
b, ok = e.Format(string(TextHclogSinkFormat))
|
||||
}
|
||||
t.Log(string(b))
|
||||
require.True(ok)
|
||||
for _, txt := range tt.want {
|
||||
assert.Contains(string(b), txt)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_hclogFormatterFilter_Name(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("simple", func(t *testing.T) {
|
||||
ff := &hclogFormatterFilter{}
|
||||
assert.Equal(t, hclogNodeName, ff.Name())
|
||||
})
|
||||
}
|
||||
|
||||
func Test_hclogFormatterFilter_Reopen(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("simple", func(t *testing.T) {
|
||||
ff := &hclogFormatterFilter{}
|
||||
assert.Equal(t, nil, ff.Reopen())
|
||||
})
|
||||
}
|
||||
|
||||
func Test_hclogFormatterFilter_Type(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("simple", func(t *testing.T) {
|
||||
ff := &hclogFormatterFilter{}
|
||||
assert.Equal(t, eventlogger.NodeTypeFormatterFilter, ff.Type())
|
||||
})
|
||||
}
|
||||
|
||||
func Test_newHclogFormatterFilter(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
jsonFormat bool
|
||||
opt []Option
|
||||
wantErr bool
|
||||
wantIsError error
|
||||
wantErrContains string
|
||||
wantAllow []string
|
||||
wantDeny []string
|
||||
}{
|
||||
{
|
||||
name: "no-opts",
|
||||
},
|
||||
{
|
||||
name: "bad-allow-filter",
|
||||
jsonFormat: true,
|
||||
opt: []Option{
|
||||
WithAllow("foo=;22", "foo==bar"),
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrContains: "invalid allow filter 'foo=;22'",
|
||||
},
|
||||
{
|
||||
name: "bad-deny-filter",
|
||||
jsonFormat: true,
|
||||
opt: []Option{
|
||||
WithDeny("foo=;22", "foo==bar"),
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrContains: "invalid deny filter 'foo=;22'",
|
||||
},
|
||||
{
|
||||
name: "empty-allow-filter",
|
||||
jsonFormat: true,
|
||||
opt: []Option{
|
||||
WithAllow(""),
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrContains: "missing filter",
|
||||
},
|
||||
{
|
||||
name: "empty-deny-filter",
|
||||
jsonFormat: true,
|
||||
opt: []Option{
|
||||
WithDeny(""),
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrContains: "missing filter",
|
||||
},
|
||||
{
|
||||
name: "valid-filters",
|
||||
jsonFormat: true,
|
||||
opt: []Option{
|
||||
WithAllow("alice==friend", "bob==friend"),
|
||||
WithDeny("eve==acquaintance", "fido!=dog"),
|
||||
},
|
||||
wantAllow: []string{"alice==friend", "bob==friend"},
|
||||
wantDeny: []string{"eve==acquaintance", "fido!=dog"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
got, err := newHclogFormatterFilter(tt.jsonFormat, tt.opt...)
|
||||
if tt.wantErr {
|
||||
require.Error(err)
|
||||
assert.Nil(got)
|
||||
if tt.wantIsError != nil {
|
||||
assert.ErrorIs(err, tt.wantIsError)
|
||||
}
|
||||
if tt.wantErrContains != "" {
|
||||
assert.Contains(err.Error(), tt.wantErrContains)
|
||||
}
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.NotNil(got)
|
||||
|
||||
assert.Equal(tt.jsonFormat, got.jsonFormat)
|
||||
|
||||
assert.Len(got.allow, len(tt.wantAllow))
|
||||
for _, f := range got.allow {
|
||||
assert.Contains(tt.wantAllow, f.raw)
|
||||
}
|
||||
assert.Len(got.deny, len(tt.wantDeny))
|
||||
for _, f := range got.deny {
|
||||
assert.Contains(tt.wantDeny, f.raw)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Reference in new issue