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/event/context.go

415 lines
12 KiB

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package event
import (
"context"
"fmt"
"os"
"runtime"
"time"
"github.com/hashicorp/boundary/internal/test"
)
var (
ci bool
isTest bool
)
type key int
const cancelledSendTimeout = 3 * time.Second
const (
eventerKey key = iota
requestInfoKey
correlationIdKey
)
func init() {
ci = os.Getenv("CI") != ""
isTest = test.IsTestRun()
}
// 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)
}
if info.EventId == "" {
return nil, fmt.Errorf("%s: missing request info event 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
}
// NewCorrelationIdContext will return a context containing a value for the
// provided CorrelationId
func NewCorrelationIdContext(ctx context.Context, correlationId string) (context.Context, error) {
const op = "event.NewCorrelationContext"
if ctx == nil {
return nil, fmt.Errorf("%s: missing context: %w", op, ErrInvalidParameter)
}
if correlationId == "" {
return nil, fmt.Errorf("%s: missing correlation id: %w", op, ErrInvalidParameter)
}
return context.WithValue(ctx, correlationIdKey, correlationId), nil
}
// CorrelationIdFromContext attempts to get the Correlation Id value from the context
// provided
func CorrelationIdFromContext(ctx context.Context) (string, bool) {
if ctx == nil {
return "", false
}
reqInfo, ok := ctx.Value(correlationIdKey).(string)
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 {
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 && opts.withRequest == nil && opts.withResponse == nil {
return fmt.Errorf("%s: specify either header or details options or request or response for an event payload: "+
"%w", op, ErrInvalidParameter)
}
// For the case that the telemetry is not enabled, and we have events coming from interceptors.
// or the case that incoming call only has withDetails without enabling telemetry.
if !eventer.conf.TelemetryEnabled && (opts.withRequest != nil || opts.withResponse != nil || opts.withDetails != nil) {
return nil
}
// If telemetry is enabled, we add it to the options.
if eventer.conf.TelemetryEnabled {
opt = append(opt, WithTelemetry())
}
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)
}
sendCtx, sendCancel := newSendCtx(ctx)
if sendCancel != nil {
defer sendCancel()
}
if err := eventer.writeObservation(sendCtx, 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. Note: any multierror errors
// will be converted to a stdlib error (because multierror doesn't support json
// marshaling).
//
// The options WithInfoMsg, WithInfo, WithId and WithRequestInfo are supported
// and all other options are ignored.
func WriteError(ctx context.Context, caller Op, e error, opt ...Option) {
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 {
switch {
case !ci:
case !isTest:
default:
fallbackLogger().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
}
sendCtx, sendCancel := newSendCtx(ctx)
if sendCancel != nil {
defer sendCancel()
}
if err := eventer.writeError(sendCtx, 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 {
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)
}
}
if correlationId, ok := CorrelationIdFromContext(ctx); ok {
opt = append(opt, withCorrelationId(correlationId))
}
e, err := newAudit(caller, opt...)
if err != nil {
return fmt.Errorf("%s: %w", op, err)
}
sendCtx, sendCancel := newSendCtx(ctx)
if sendCancel != nil {
defer sendCancel()
}
if err := eventer.writeAudit(sendCtx, 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
// event and we'll generate one and flush the event
// since there will never be another with the same id
id, err := NewId(IdPrefix)
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))
switch reqInfo.EventId {
case "":
// there's no RequestInfo.EventId 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("e")
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.EventId))
}
}
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. The args are and optional set of key/value pairs about the event.
//
// This function should never be used when sending events while
// handling API requests.
func WriteSysEvent(ctx context.Context, caller Op, msg string, args ...any) {
const op = "event.WriteSysEvent"
info := ConvertArgs(args...)
if msg == "" && info == nil {
return
}
if info == nil {
info = make(map[string]any, 1)
}
info[msgField] = msg
if caller == "" {
pc, _, _, ok := runtime.Caller(1)
details := runtime.FuncForPC(pc)
if ok && details != nil {
caller = Op(details.Name())
} else {
caller = "unknown operation"
}
}
eventer, ok := EventerFromContext(ctx)
if !ok {
eventer = SysEventer()
if eventer == nil {
fallbackLogger().Error(fmt.Sprintf("%s: no eventer available to write sysevent: (%s) %+v", op, caller, info))
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, info))
}
e := &sysEvent{
Id: Id(id),
Version: sysVersion,
Op: caller,
Data: info,
}
sendCtx, sendCancel := newSendCtx(ctx)
if sendCancel != nil {
defer sendCancel()
}
if err := eventer.writeSysEvent(sendCtx, 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
}
}
func newSendCtx(ctx context.Context) (context.Context, context.CancelFunc) {
var sendCtx context.Context
var sendCancel context.CancelFunc
switch {
case ctx == nil:
return context.Background(), nil
case ctx.Err() == context.Canceled || ctx.Err() == context.DeadlineExceeded:
sendCtx, sendCancel = context.WithTimeout(context.Background(), cancelledSendTimeout)
info, ok := RequestInfoFromContext(ctx)
if ok {
reqCtx, err := NewRequestInfoContext(sendCtx, info)
if err == nil {
sendCtx = reqCtx
}
}
default:
sendCtx = ctx
}
return sendCtx, sendCancel
}
// MissingKey defines a key to be used as the "missing key" when ConvertArgs has
// an odd number of args (it's missing a key in its key/value pairs)
const MissingKey = "EXTRA_VALUE_AT_END"
// ConvertArgs will convert the key/value pair args to a map. If the args
// provided are an odd number (they're missing a key in their key/value pairs)
// then MissingKey is used to the missing key.
func ConvertArgs(args ...any) map[string]any {
if len(args) == 0 {
return nil
}
if len(args)%2 != 0 {
extra := args[len(args)-1]
args = append(args[:len(args)-1], MissingKey, extra)
}
m := map[string]any{}
for i := 0; i < len(args); i = i + 2 {
var key string
switch st := args[i].(type) {
case string:
key = st
default:
key = fmt.Sprintf("%v", st)
}
m[key] = args[i+1]
}
return m
}