Files
threshold/internal/observability/event/context.go
T

295 lines
9.1 KiB
Go

package event
import (
"context"
"fmt"
"os"
"runtime"
"strings"
"github.com/hashicorp/boundary/globals"
"github.com/hashicorp/go-hclog"
)
type key int
const (
eventerKey key = iota
requestInfoKey
)
// NewEventerContext will return a context containing a value of the provided Eventer
func NewEventerContext(ctx context.Context, eventer *Eventer) (context.Context, error) {
const op = "event.NewEventerContext"
if ctx == nil {
return nil, fmt.Errorf("%s: missing context: %w", op, ErrInvalidParameter)
}
if eventer == nil {
return nil, fmt.Errorf("%s: missing eventer: %w", op, ErrInvalidParameter)
}
return context.WithValue(ctx, eventerKey, eventer), nil
}
// EventerFromContext attempts to get the eventer value from the context provided
func EventerFromContext(ctx context.Context) (*Eventer, bool) {
if ctx == nil {
return nil, false
}
eventer, ok := ctx.Value(eventerKey).(*Eventer)
return eventer, ok
}
// NewRequestInfoContext will return a context containing a value for the
// provided RequestInfo
func NewRequestInfoContext(ctx context.Context, info *RequestInfo) (context.Context, error) {
const op = "event.NewRequestInfoContext"
if ctx == nil {
return nil, fmt.Errorf("%s: missing context: %w", op, ErrInvalidParameter)
}
if info == nil {
return nil, fmt.Errorf("%s: missing request info: %w", op, ErrInvalidParameter)
}
if info.Id == "" {
return nil, fmt.Errorf("%s: missing request info id: %w", op, ErrInvalidParameter)
}
return context.WithValue(ctx, requestInfoKey, info), nil
}
// RequestInfoFromContext attempts to get the RequestInfo value from the context
// provided
func RequestInfoFromContext(ctx context.Context) (*RequestInfo, bool) {
if ctx == nil {
return nil, false
}
reqInfo, ok := ctx.Value(requestInfoKey).(*RequestInfo)
return reqInfo, ok
}
// WriteObservation will write an observation event. It will first check the
// ctx for an eventer, then try event.SysEventer() and if no eventer can be
// found an error is returned.
//
// At least one and any combination of the supported options may be used:
// WithHeader, WithDetails, WithId, WithFlush and WithRequestInfo. All other
// options are ignored.
func WriteObservation(ctx context.Context, caller Op, opt ...Option) error {
// TODO (jimlambrt) 6/2021: remove this feature flag envvar when events are
// generally available.
if !strings.EqualFold(os.Getenv(globals.BOUNDARY_DEVELOPER_ENABLE_EVENTS), "true") {
return nil
}
const op = "event.WriteObservation"
if ctx == nil {
return fmt.Errorf("%s: missing context: %w", op, ErrInvalidParameter)
}
if caller == "" {
return fmt.Errorf("%s: missing operation: %w", op, ErrInvalidParameter)
}
eventer, ok := EventerFromContext(ctx)
if !ok {
eventer = SysEventer()
if eventer == nil {
return fmt.Errorf("%s: missing both context and system eventer: %w", op, ErrInvalidParameter)
}
}
opts := getOpts(opt...)
if opts.withDetails == nil && opts.withHeader == nil && !opts.withFlush {
return fmt.Errorf("%s: specify either header or details options for an event payload: %w", op, ErrInvalidParameter)
}
if opts.withRequestInfo == nil {
var err error
if opt, err = addCtxOptions(ctx, opt...); err != nil {
return fmt.Errorf("%s: %w", op, err)
}
}
e, err := newObservation(caller, opt...)
if err != nil {
return fmt.Errorf("%s: %w", op, err)
}
if err := eventer.writeObservation(ctx, e); err != nil {
return fmt.Errorf("%s: %w", op, err)
}
return nil
}
// WriteError will write an error event. It will first check the
// ctx for an eventer, then try event.SysEventer() and if no eventer can be
// found an hclog.Logger will be created and used.
//
// The options WithId and WithRequestInfo are supported and all other options
// are ignored.
func WriteError(ctx context.Context, caller Op, e error, opt ...Option) {
// TODO (jimlambrt) 6/2021: remove this feature flag envvar when events are
// generally available.
if !strings.EqualFold(os.Getenv(globals.BOUNDARY_DEVELOPER_ENABLE_EVENTS), "true") {
return
}
const op = "event.WriteError"
// EventerFromContext will handle a nil ctx appropriately. If e or caller is
// missing, newError(...) will handle them appropriately.
eventer, ok := EventerFromContext(ctx)
if !ok {
eventer = SysEventer()
if eventer == nil {
logger := hclog.New(nil)
logger.Error(fmt.Sprintf("%s: no eventer available to write error: %v", op, e))
return
}
}
opts := getOpts(opt...)
if opts.withRequestInfo == nil {
var err error
if opt, err = addCtxOptions(ctx, opt...); err != nil {
eventer.logger.Error(fmt.Sprintf("%s: %v", op, err))
eventer.logger.Error(fmt.Sprintf("%s: unable to process context options to write error: %v", op, e))
return
}
}
ev, err := newError(caller, e, opt...)
if err != nil {
eventer.logger.Error(fmt.Sprintf("%s: %v", op, err))
eventer.logger.Error(fmt.Sprintf("%s: unable to create new error to write error: %v", op, e))
return
}
if err := eventer.writeError(ctx, ev); err != nil {
eventer.logger.Error(fmt.Sprintf("%s: %v", op, err))
eventer.logger.Error(fmt.Sprintf("%s: unable to write error: %v", op, e))
return
}
}
// WriteAudit will write an audit event. It will first check the ctx for an
// eventer, then try event.SysEventer() and if no eventer can be found an error
// is returned.
//
// At least one and any combination of the supported options may be used:
// WithRequest, WithResponse, WithAuth, WithId, WithFlush and WithRequestInfo.
// All other options are ignored.
func WriteAudit(ctx context.Context, caller Op, opt ...Option) error {
// TODO (jimlambrt) 6/2021: remove this feature flag envvar when events are
// generally available.
if !strings.EqualFold(os.Getenv(globals.BOUNDARY_DEVELOPER_ENABLE_EVENTS), "true") {
return nil
}
const op = "event.WriteAudit"
if ctx == nil {
return fmt.Errorf("%s: missing context: %w", op, ErrInvalidParameter)
}
if caller == "" {
return fmt.Errorf("%s: missing operation: %w", op, ErrInvalidParameter)
}
eventer, ok := EventerFromContext(ctx)
if !ok {
eventer = SysEventer()
if eventer == nil {
return fmt.Errorf("%s: missing both context and system eventer: %w", op, ErrInvalidParameter)
}
}
opts := getOpts(opt...)
if opts.withRequestInfo == nil {
var err error
if opt, err = addCtxOptions(ctx, opt...); err != nil {
return fmt.Errorf("%s: %w", op, err)
}
}
e, err := newAudit(caller, opt...)
if err != nil {
return fmt.Errorf("%s: %w", op, err)
}
if err := eventer.writeAudit(ctx, e); err != nil {
return fmt.Errorf("%s: %w", op, err)
}
return nil
}
func addCtxOptions(ctx context.Context, opt ...Option) ([]Option, error) {
const op = "event.addCtxOptions"
opts := getOpts(opt...)
retOpts := make([]Option, 0, len(opt))
retOpts = append(retOpts, opt...)
if opts.withRequestInfo == nil {
reqInfo, ok := RequestInfoFromContext(ctx)
if !ok {
// there's no RequestInfo, so there's no id associated with the
// observation and we'll generate one and flush the observation
// since there will never be another with the same id
id, err := newId(string(ObservationType))
if err != nil {
return nil, fmt.Errorf("%s: %w", op, err)
}
retOpts = append(retOpts, WithId(id))
if !opts.withFlush {
retOpts = append(retOpts, WithFlush())
}
return retOpts, nil
}
retOpts = append(retOpts, WithRequestInfo(reqInfo))
if reqInfo.Id != "" {
retOpts = append(retOpts, WithId(reqInfo.Id))
}
switch reqInfo.Id {
case "":
// there's no RequestInfo.Id associated with the observation, so we'll
// generate one and flush the observation since there will never be
// another with the same id
id, err := newId(string(ObservationType))
if err != nil {
return nil, fmt.Errorf("%s: %w", op, err)
}
retOpts = append(retOpts, WithId(id))
if !opts.withFlush {
retOpts = append(retOpts, WithFlush())
}
return retOpts, nil
default:
retOpts = append(retOpts, WithId(reqInfo.Id))
}
}
return retOpts, nil
}
// WriteSysEvent will write a sysevent using the eventer from
// event.SysEventer() if no eventer can be found an hclog.Logger will be created
// and used. This function should never be used when sending events while
// handling API requests.
func WriteSysEvent(ctx context.Context, caller Op, data map[string]interface{}) {
const op = "event.WriteError"
if data == nil {
return
}
if caller == "" {
pc, _, _, ok := runtime.Caller(1)
details := runtime.FuncForPC(pc)
if ok && details != nil {
caller = Op(details.Name())
} else {
caller = "unknown operation"
}
}
eventer := SysEventer()
if eventer == nil {
logger := hclog.New(nil)
logger.Error(fmt.Sprintf("%s: no eventer available to write sysevent: (%s) %+v", op, caller, data))
return
}
id, err := newId(string(SystemType))
if err != nil {
eventer.logger.Error(fmt.Sprintf("%s: %v", op, err))
eventer.logger.Error(fmt.Sprintf("%s: unable to generate id while writing sysevent: (%s) %+v", op, caller, data))
}
e := &sysEvent{
Id: Id(id),
Version: sysVersion,
Op: caller,
Data: data,
}
if err := eventer.writeSysEvent(ctx, e); err != nil {
eventer.logger.Error(fmt.Sprintf("%s: %v", op, err))
eventer.logger.Error(fmt.Sprintf("%s: unable to write sysevent: (%s) %+v", op, caller, e))
return
}
}