// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package event import ( "bytes" "context" "errors" "fmt" "io" "log" "net/url" "os" "reflect" "runtime" "strings" "sync" "time" "github.com/hashicorp/eventlogger" "github.com/hashicorp/eventlogger/filters/encrypt" "github.com/hashicorp/eventlogger/filters/gated" "github.com/hashicorp/eventlogger/formatter_filters/cloudevents" "github.com/hashicorp/eventlogger/sinks/writer" "github.com/hashicorp/go-hclog" wrapping "github.com/hashicorp/go-kms-wrapping/v2" "go.uber.org/atomic" "google.golang.org/protobuf/types/known/fieldmaskpb" ) const ( OpField = "op" // OpField in an event. RequestInfoField = "request_info" // RequestInfoField in an event. VersionField = "version" // VersionField in an event DetailsField = "details" // Details field in an event. HeaderField = "header" // HeaderField in an event. IdField = "id" // IdField in an event. CreatedAtField = "created_at" // CreatedAtField in an event. TypeField = "type" // TypeField in an event. RequestField = "request" // Request field in an event. ResponseField = "response" // Response field in an event. auditPipeline = "audit-pipeline" // auditPipeline is a pipeline for audit events observationPipeline = "observation-pipeline" // observationPipeline is a pipeline for observation events errPipeline = "err-pipeline" // errPipeline is a pipeline for error events sysPipeline = "sys-pipeline" // sysPipeline is a pipeline for system events ) // flushable defines an interface that all eventlogger Nodes must implement if // they are "flushable" type flushable interface { FlushAll(ctx context.Context) error } // broker defines an interface for an eventlogger Broker... which will allow us // to substitute our testing broker when needed to write tests for things // like event send retrying. type broker interface { Send(ctx context.Context, t eventlogger.EventType, payload any) (eventlogger.Status, error) Reopen(ctx context.Context) error StopTimeAt(now time.Time) RegisterNode(id eventlogger.NodeID, node eventlogger.Node, opt ...eventlogger.Option) error RemoveNode(ctx context.Context, id eventlogger.NodeID) error SetSuccessThreshold(t eventlogger.EventType, successThreshold int) error RegisterPipeline(def eventlogger.Pipeline, opt ...eventlogger.Option) error RemovePipelineAndNodes(ctx context.Context, t eventlogger.EventType, id eventlogger.PipelineID) (bool, error) } // queuedEvent stores an event and the context that was associated with it when // gating type queuedEvent struct { ctx context.Context event any } // Eventer provides a method to send events to pipelines of sinks type Eventer struct { broker broker serverName string flushableNodes []flushable conf EventerConfig logger hclog.Logger auditPipelines []pipeline observationPipelines []pipeline errPipelines []pipeline auditWrapperNodes []any // Gating is used to delay output of events until after we have a chance to // render startup info, similar to what was done for hclog before eventing // supplanted it. It affects only error and system events. gated atomic.Bool gatedQueue []*queuedEvent gatedQueueLock *sync.Mutex } type node struct { node eventlogger.Node id eventlogger.NodeID } type pipeline struct { eventType Type fmtId eventlogger.NodeID sinkId eventlogger.NodeID gateId eventlogger.NodeID encryptFilterId eventlogger.NodeID sinkConfig *SinkConfig } var ( sysEventer *Eventer // sysEventer is the system-wide Eventer sysEventerLock sync.RWMutex // sysEventerLock allows the sysEventer to safely be written concurrently. sysFallbackLogger struct { mu sync.RWMutex logger hclog.Logger } ) func defaultLogger() hclog.Logger { opts := new(hclog.LoggerOptions) *opts = *hclog.DefaultOptions opts.JSONFormat = true return hclog.New(opts) } // fallbackLogger returns the current fallback logger for eventing. If there's // no current fallback logger, then a default logger is returned. func fallbackLogger() hclog.Logger { sysFallbackLogger.mu.RLock() defer sysFallbackLogger.mu.RUnlock() if sysFallbackLogger.logger == nil { return defaultLogger() } return sysFallbackLogger.logger } // InitFallbackLogger will initialize the fallback logger for eventing func InitFallbackLogger(l hclog.Logger) error { const op = "event.InitFallbackLogger" if isNil(l) { return fmt.Errorf("%s: missing logger: %w", op, ErrInvalidParameter) } sysFallbackLogger.mu.Lock() defer sysFallbackLogger.mu.Unlock() sysFallbackLogger.logger = l return nil } // InitSysEventer provides a mechanism to initialize a "system wide" eventer // singleton for Boundary. Support the options of: WithEventer(...) and // WithEventerConfig(...) // // IMPORTANT: Eventers cannot share file sinks, which likely means that each // process should only have one Eventer. In practice this means the process // Server (Controller or Worker) and the SysEventer both need a pointer to a // single Eventer. func InitSysEventer(log hclog.Logger, serializationLock *sync.Mutex, serverName string, opt ...Option) error { const op = "event.InitSysEventer" if log == nil { return fmt.Errorf("%s: missing hclog: %w", op, ErrInvalidParameter) } if serializationLock == nil { return fmt.Errorf("%s: missing serialization lock: %w", op, ErrInvalidParameter) } if serverName == "" { return fmt.Errorf("%s: missing server name: %w", op, ErrInvalidParameter) } // the order of operations is important here. we want to determine if // there's an error before setting the singleton sysEventer var e *Eventer opts := getOpts(opt...) switch { case opts.withEventer == nil && opts.withEventerConfig == nil: return fmt.Errorf("%s: missing both eventer and eventer config: %w", op, ErrInvalidParameter) case opts.withEventer != nil && opts.withEventerConfig != nil: return fmt.Errorf("%s: both eventer and eventer config provided: %w", op, ErrInvalidParameter) case opts.withEventerConfig != nil: var err error if e, err = NewEventer(log, serializationLock, serverName, *opts.withEventerConfig); err != nil { return fmt.Errorf("%s: %w", op, err) } case opts.withEventer != nil: e = opts.withEventer } sysEventerLock.Lock() defer sysEventerLock.Unlock() sysEventer = e return nil } // SysEventer returns the "system wide" eventer for Boundary and can/will return // a nil Eventer func SysEventer() *Eventer { sysEventerLock.RLock() defer sysEventerLock.RUnlock() return sysEventer } // NewAuditEncryptFilter returns a new encrypt filter which is initialized for // audit events. func NewAuditEncryptFilter(opt ...Option) (*encrypt.Filter, error) { opts := getOpts(opt...) return &encrypt.Filter{ Wrapper: opts.withAuditWrapper, IgnoreTypes: []reflect.Type{ reflect.TypeOf(&fieldmaskpb.FieldMask{}), }, }, nil } // NewEventer creates a new Eventer using the config. Supports options: // WithNow, WithSerializationLock, WithBroker, WithAuditWrapper, // WithNoDefaultSink func NewEventer(log hclog.Logger, serializationLock *sync.Mutex, serverName string, c EventerConfig, opt ...Option) (*Eventer, error) { const op = "event.NewEventer" if log == nil { return nil, fmt.Errorf("%s: missing logger: %w", op, ErrInvalidParameter) } if serializationLock == nil { return nil, fmt.Errorf("%s: missing serialization lock: %w", op, ErrInvalidParameter) } if serverName == "" { return nil, fmt.Errorf("%s: missing server name: %w", op, ErrInvalidParameter) } opts := getOpts(opt...) // if there are no sinks in config, then we'll default to just one stderr // sink. if len(c.Sinks) == 0 && !opts.withNoDefaultSink { c.Sinks = append(c.Sinks, DefaultSink()) } if err := c.Validate(); err != nil { return nil, fmt.Errorf("%s: %w", op, err) } var auditPipelines, observationPipelines, errPipelines, sysPipelines []pipeline var b broker switch { case opts.withBroker != nil: b = opts.withBroker default: b, _ = eventlogger.NewBroker() } e := &Eventer{ gatedQueueLock: new(sync.Mutex), logger: log, conf: c, broker: b, auditWrapperNodes: []any{}, serverName: serverName, } if !opts.withNow.IsZero() { e.broker.StopTimeAt(opts.withNow) } if opts.withGating { e.gated.Store(true) } // serializedStderr will be shared among all StderrSinks so their output is not // interwoven serializedStderr := serializedWriter{ w: os.Stderr, l: serializationLock, } // we need to keep track of all the Sink filenames to ensure they aren't // reused. allSinkFilenames := map[string]bool{} for _, s := range c.Sinks { fmtId, fmtNode, err := newFmtFilterNode(serverName, *s, opt...) e.auditWrapperNodes = append(e.auditWrapperNodes, fmtNode) if err != nil { return nil, fmt.Errorf("%s: %w", op, err) } err = e.broker.RegisterNode(eventlogger.NodeID(fmtId), fmtNode) if err != nil { return nil, fmt.Errorf("%s: unable to register fmt/filter node: %w", op, err) } var sinkId eventlogger.NodeID var sinkNode eventlogger.Node switch s.Type { case StderrSink: sinkNode = &writer.Sink{ Format: string(s.Format), Writer: &serializedStderr, } id, err := NewId("stderr") if err != nil { return nil, fmt.Errorf("%s: %w", op, err) } sinkId = eventlogger.NodeID(id) case FileSink: fsc := s.FileConfig if _, found := allSinkFilenames[fsc.Path+fsc.FileName]; found { return nil, fmt.Errorf("%s: duplicate file sink: %s %s: %w", op, fsc.Path, fsc.FileName, ErrInvalidParameter) } allSinkFilenames[fsc.Path+fsc.FileName] = true sinkNode = &eventlogger.FileSink{ Format: string(s.Format), Path: fsc.Path, FileName: fsc.FileName, MaxBytes: fsc.RotateBytes, MaxDuration: fsc.RotateDuration, MaxFiles: fsc.RotateMaxFiles, } id, err := NewId(fmt.Sprintf("file_%s_%s_", fsc.Path, fsc.FileName)) if err != nil { return nil, fmt.Errorf("%s: %w", op, err) } sinkId = eventlogger.NodeID(id) case WriterSink: wsc := s.WriterConfig sinkNode = &writer.Sink{ Format: string(s.Format), Writer: wsc.Writer, } id, err := NewId("writer") if err != nil { return nil, fmt.Errorf("%s: %w", op, err) } sinkId = eventlogger.NodeID(id) default: return nil, fmt.Errorf("%s: unknown sink type %s", op, s.Type) } err = e.broker.RegisterNode(sinkId, sinkNode) if err != nil { return nil, fmt.Errorf("%s: failed to register sink node %s: %w", op, sinkId, err) } var addToAudit, addToObservation, addToErr, addToSys bool for _, t := range s.EventTypes { switch t { case EveryType: addToAudit = true addToObservation = true addToErr = true addToSys = true case ErrorType: addToErr = true case AuditType: addToAudit = true case ObservationType: addToObservation = true case SystemType: addToSys = true } } if addToAudit { var fop AuditFilterOperations if s.AuditConfig != nil { fop = s.AuditConfig.FilterOverrides } s.AuditConfig, err = NewAuditConfig(WithAuditWrapper(opts.withAuditWrapper), WithFilterOperations(fop)) if err != nil { return nil, fmt.Errorf("%s: %w", op, err) } encryptFilter, err := NewAuditEncryptFilter(opt...) if err != nil { return nil, fmt.Errorf("%s: %w", op, err) } e.auditWrapperNodes = append(e.auditWrapperNodes, encryptFilter) if len(s.AuditConfig.FilterOverrides) > 0 { overrides := encrypt.DefaultFilterOperations() for k, v := range s.AuditConfig.FilterOverrides { overrides[encrypt.DataClassification(k)] = encrypt.FilterOperation(v) } encryptFilter.FilterOperationOverrides = overrides } id, err := NewId("encrypt-audit") if err != nil { return nil, fmt.Errorf("%s: %w", op, err) } encryptFilterId := eventlogger.NodeID(id) if err := b.RegisterNode(encryptFilterId, encryptFilter); err != nil { return nil, fmt.Errorf("%s: %w", op, err) } auditPipelines = append(auditPipelines, pipeline{ eventType: AuditType, fmtId: fmtId, sinkId: sinkId, encryptFilterId: encryptFilterId, sinkConfig: s, }) } if addToObservation { observationPipelines = append(observationPipelines, pipeline{ eventType: ObservationType, fmtId: fmtId, sinkId: sinkId, sinkConfig: s, }) } if addToErr { errPipelines = append(errPipelines, pipeline{ eventType: ErrorType, fmtId: fmtId, sinkId: sinkId, sinkConfig: s, }) } if addToSys { sysPipelines = append(sysPipelines, pipeline{ eventType: SystemType, fmtId: fmtId, sinkId: sinkId, }) } } if c.AuditEnabled && len(auditPipelines) == 0 { return nil, fmt.Errorf("%s: audit events enabled but no sink defined for it: %w", op, ErrInvalidParameter) } if c.ObservationsEnabled && len(observationPipelines) == 0 { return nil, fmt.Errorf("%s: observation events enabled but no sink defined for it: %w", op, ErrInvalidParameter) } if c.SysEventsEnabled && len(sysPipelines) == 0 { return nil, fmt.Errorf("%s: system events enabled but no sink defined for it: %w", op, ErrInvalidParameter) } auditNodeIds := make([]eventlogger.NodeID, 0, len(auditPipelines)) for _, p := range auditPipelines { gatedFilterNode := gated.Filter{ Broker: e.broker, } e.flushableNodes = append(e.flushableNodes, &gatedFilterNode) gateId, err := NewId("gated-audit") if err != nil { return nil, fmt.Errorf("%s: %w", op, err) } p.gateId = eventlogger.NodeID(gateId) if err := e.broker.RegisterNode(p.gateId, &gatedFilterNode); err != nil { return nil, fmt.Errorf("%s: unable to register audit gated filter: %w", op, err) } pipeId, err := NewId(auditPipeline) if err != nil { return nil, fmt.Errorf("%s: %w", op, err) } err = e.broker.RegisterPipeline(eventlogger.Pipeline{ EventType: eventlogger.EventType(p.eventType), PipelineID: eventlogger.PipelineID(pipeId), // order of nodes is important! gate (aggregate), then filter/format, then encrypt, then write to sink NodeIDs: []eventlogger.NodeID{p.gateId, p.encryptFilterId, p.fmtId, p.sinkId}, }) if err != nil { return nil, fmt.Errorf("%s: failed to register audit pipeline: %w", op, err) } auditNodeIds = append(auditNodeIds, p.sinkId) } observationNodeIds := make([]eventlogger.NodeID, 0, len(observationPipelines)) for _, p := range observationPipelines { gatedFilterNode := gated.Filter{ Broker: e.broker, } e.flushableNodes = append(e.flushableNodes, &gatedFilterNode) gateId, err := NewId("gated-observation") if err != nil { return nil, fmt.Errorf("%s: %w", op, err) } p.gateId = eventlogger.NodeID(gateId) if err := e.broker.RegisterNode(p.gateId, &gatedFilterNode); err != nil { return nil, fmt.Errorf("%s: unable to register audit gated filter: %w", op, err) } pipeId, err := NewId(observationPipeline) if err != nil { return nil, fmt.Errorf("%s: %w", op, err) } err = e.broker.RegisterPipeline(eventlogger.Pipeline{ EventType: eventlogger.EventType(p.eventType), PipelineID: eventlogger.PipelineID(pipeId), // order of nodes is important! gate (aggregate), then filter/format, then write to sink NodeIDs: []eventlogger.NodeID{p.gateId, p.fmtId, p.sinkId}, }) if err != nil { return nil, fmt.Errorf("%s: failed to register observation pipeline: %w", op, err) } observationNodeIds = append(observationNodeIds, p.sinkId) } errNodeIds := make([]eventlogger.NodeID, 0, len(errPipelines)) for _, p := range errPipelines { pipeId, err := NewId(errPipeline) if err != nil { return nil, fmt.Errorf("%s: %w", op, err) } err = e.broker.RegisterPipeline(eventlogger.Pipeline{ EventType: eventlogger.EventType(p.eventType), PipelineID: eventlogger.PipelineID(pipeId), // order of nodes is important! filter/format, then write to sink NodeIDs: []eventlogger.NodeID{p.fmtId, p.sinkId}, }) if err != nil { return nil, fmt.Errorf("%s: failed to register err pipeline: %w", op, err) } errNodeIds = append(errNodeIds, p.sinkId) } sysNodeIds := make([]eventlogger.NodeID, 0, len(sysPipelines)) for _, p := range sysPipelines { pipeId, err := NewId(sysPipeline) if err != nil { return nil, fmt.Errorf("%s: %w", op, err) } err = e.broker.RegisterPipeline(eventlogger.Pipeline{ EventType: eventlogger.EventType(p.eventType), PipelineID: eventlogger.PipelineID(pipeId), // order of nodes is important! filter/format, then write to sink NodeIDs: []eventlogger.NodeID{p.fmtId, p.sinkId}, }) if err != nil { return nil, fmt.Errorf("%s: failed to register sys pipeline: %w", op, err) } sysNodeIds = append(sysNodeIds, p.sinkId) } err := e.broker.SetSuccessThreshold(eventlogger.EventType(ObservationType), len(observationNodeIds)) if err != nil { return nil, fmt.Errorf("%s: failed to set success threshold for observation events: %w", op, err) } err = e.broker.SetSuccessThreshold(eventlogger.EventType(AuditType), len(auditNodeIds)) if err != nil { return nil, fmt.Errorf("%s: failed to set success threshold for audit events: %w", op, err) } err = e.broker.SetSuccessThreshold(eventlogger.EventType(ErrorType), len(errNodeIds)) if err != nil { return nil, fmt.Errorf("%s: failed to set success threshold for error events: %w", op, err) } err = e.broker.SetSuccessThreshold(eventlogger.EventType(SystemType), len(sysNodeIds)) if err != nil { return nil, fmt.Errorf("%s: failed to set success threshold for sysevents: %w", op, err) } e.auditPipelines = append(e.auditPipelines, auditPipelines...) e.errPipelines = append(e.errPipelines, errPipelines...) e.observationPipelines = append(e.observationPipelines, observationPipelines...) return e, nil } func newFmtFilterNode(serverName string, c SinkConfig, opt ...Option) (eventlogger.NodeID, eventlogger.Node, error) { const op = "newFmtFilterNode" if serverName == "" { return "", nil, fmt.Errorf("%s: missing server name: %w", op, ErrInvalidParameter) } opts := getOpts(opt...) var fmtId eventlogger.NodeID var fmtNode eventlogger.Node switch c.Format { case TextHclogSinkFormat, JSONHclogSinkFormat: id, err := NewId(string(c.Format)) if err != nil { return "", nil, fmt.Errorf("%s: unable to generate id: %w", op, err) } fmtId = eventlogger.NodeID(id) fmtNode, err = newHclogFormatterFilter(c.Format == JSONHclogSinkFormat, WithAllow(c.AllowFilters...), WithDeny(c.DenyFilters...), WithAuditWrapper(opts.withAuditWrapper)) if err != nil { return "", nil, fmt.Errorf("%s: %w", op, err) } default: id, err := NewId("cloudevents") if err != nil { return "", nil, fmt.Errorf("%s: unable to generate id: %w", op, err) } fmtId = eventlogger.NodeID(id) var sourceUrl *url.URL switch { case c.EventSourceUrl != "": sourceUrl, err = url.Parse(c.EventSourceUrl) if err != nil { return "", nil, fmt.Errorf("%s: invalid event source URL (%s): %w", op, c.EventSourceUrl, err) } default: s := fmt.Sprintf("https://hashicorp.com/boundary/%s", serverName) sourceUrl, err = url.Parse(s) if err != nil { return "", nil, fmt.Errorf("%s: invalid event source URL (%s): %w", op, s, err) } } fmtNode, err = newCloudEventsFormatterFilter(sourceUrl, cloudevents.Format(c.Format), WithAllow(c.AllowFilters...), WithDeny(c.DenyFilters...)) if err != nil { return "", nil, fmt.Errorf("%s: %w", op, err) } } return fmtId, fmtNode, nil } func DefaultEventerConfig() *EventerConfig { return &EventerConfig{ AuditEnabled: false, TelemetryEnabled: false, ObservationsEnabled: true, SysEventsEnabled: true, Sinks: []*SinkConfig{DefaultSink()}, } } func DefaultSink() *SinkConfig { return &SinkConfig{ Name: "default", EventTypes: []Type{EveryType}, Format: JSONSinkFormat, Type: StderrSink, AuditConfig: DefaultAuditConfig(), } } func (e *Eventer) RotateAuditWrapper(ctx context.Context, newWrapper wrapping.Wrapper) error { const op = "event.(Eventer).RotateAuditWrapper" if newWrapper == nil { return fmt.Errorf("%s: missing wrapper: %w", op, ErrInvalidParameter) } for _, n := range e.auditWrapperNodes { switch w := n.(type) { case *hclogFormatterFilter: w.Rotate(newWrapper) case *cloudEventsFormatterFilter: w.Rotate(newWrapper) case *encrypt.Filter: w.Rotate(encrypt.WithWrapper(newWrapper)) default: return fmt.Errorf("%s: unsupported node type (%s): %w", op, reflect.TypeOf(w), ErrInvalidParameter) } } return nil } // writeObservation writes/sends an Observation event. func (e *Eventer) writeObservation(ctx context.Context, event *observation, _ ...Option) error { const op = "event.(Eventer).writeObservation" if event == nil { return fmt.Errorf("%s: missing event: %w", op, ErrInvalidParameter) } if !e.conf.ObservationsEnabled { return nil } err := e.retrySend(ctx, stdRetryCount, expBackoff{}, func() (eventlogger.Status, error) { if event.Header != nil { event.Header[RequestInfoField] = event.RequestInfo event.Header[VersionField] = event.Version } if event.Detail != nil { event.Detail[OpField] = string(event.Op) } return e.broker.Send(ctx, eventlogger.EventType(ObservationType), event) }) if err != nil { e.logger.Error("encountered an error sending an observation event", "error:", err.Error()) return fmt.Errorf("%s: %w", op, err) } return nil } // writeError writes/sends an Err event func (e *Eventer) writeError(ctx context.Context, event *err, opt ...Option) error { const op = "event.(Eventer).writeError" if event == nil { return fmt.Errorf("%s: missing event: %w", op, ErrInvalidParameter) } if e.conf.ErrorEventsDisabled { return nil } opts := getOpts(opt...) if e.gated.Load() && !opts.withNoGateLocking { e.gatedQueueLock.Lock() defer e.gatedQueueLock.Unlock() if e.gated.Load() { // validate that it's still gated and we haven't just drained the queue e.gatedQueue = append(e.gatedQueue, &queuedEvent{ ctx: ctx, event: event, }) return nil } } err := e.retrySend(ctx, stdRetryCount, expBackoff{}, func() (eventlogger.Status, error) { return e.broker.Send(ctx, eventlogger.EventType(ErrorType), event) }) if err != nil { e.logger.Error("encountered an error sending an error event", "error:", err.Error()) return fmt.Errorf("%s: %w", op, err) } return nil } // writeSysEvent writes/sends an sysEvent event func (e *Eventer) writeSysEvent(ctx context.Context, event *sysEvent, opt ...Option) error { const op = "event.(Eventer).writeSysEvent" if event == nil { return fmt.Errorf("%s: missing event: %w", op, ErrInvalidParameter) } if !e.conf.SysEventsEnabled { return nil } opts := getOpts(opt...) if e.gated.Load() && !opts.withNoGateLocking { e.gatedQueueLock.Lock() defer e.gatedQueueLock.Unlock() if e.gated.Load() { // validate that it's still gated and we haven't just drained the queue e.gatedQueue = append(e.gatedQueue, &queuedEvent{ ctx: ctx, event: event, }) return nil } } err := e.retrySend(ctx, stdRetryCount, expBackoff{}, func() (eventlogger.Status, error) { return e.broker.Send(ctx, eventlogger.EventType(SystemType), event) }) if err != nil { e.logger.Error("encountered an error sending an sys event", "error:", err.Error()) return fmt.Errorf("%s: %w", op, err) } return nil } // writeAudit writes/send an audit event func (e *Eventer) writeAudit(ctx context.Context, event *audit, _ ...Option) error { const op = "event.(Eventer).writeAudit" if event == nil { return fmt.Errorf("%s: missing event: %w", op, ErrInvalidParameter) } if !e.conf.AuditEnabled { return nil } err := e.retrySend(ctx, stdRetryCount, expBackoff{}, func() (eventlogger.Status, error) { return e.broker.Send(ctx, eventlogger.EventType(AuditType), event) }) if err != nil { e.logger.Error("encountered an error sending an audit event", "error:", err.Error()) return fmt.Errorf("%s: %w", op, err) } return nil } // Reopen can used during a SIGHUP to reopen nodes, most importantly the underlying // file sinks. func (e *Eventer) Reopen() error { if e.broker != nil { return e.broker.Reopen(context.Background()) } return nil } // FlushNodes will flush any of the eventer's flushable nodes. This // needs to be called whenever Boundary is stopping (aka shutting down). func (e *Eventer) FlushNodes(ctx context.Context) error { const op = "event.(Eventer).FlushNodes" for _, n := range e.flushableNodes { if err := n.FlushAll(ctx); err != nil { return fmt.Errorf("%s: %w", op, err) } } return nil } // ReleaseGate releases queued events. If any event isn't successfully written, // it remains in the queue and we could try a flush later. func (e *Eventer) ReleaseGate() error { const op = "event.(Eventer).ReleaseGate" e.gatedQueueLock.Lock() defer e.gatedQueueLock.Unlock() // Don't gate anymore. By the time we hit this we will have the lock and be // guaranteed that nothing else will add onto the queue while we drain it. // Logic in the functions that add to the queue validate this boolean again // after they acquire the lock so running this function should fully drain // the events except in the case of an error. e.gated.Store(false) // Don't do anything if we're not gated and are fully drained if len(e.gatedQueue) == 0 && !e.gated.Load() { return nil } var totalErrs error for _, qe := range e.gatedQueue { var writeErr error if qe == nil { continue // we may have already sent this but gotten errors later } var queuedOp string switch t := qe.event.(type) { case *sysEvent: queuedOp = "system" writeErr = e.writeSysEvent(qe.ctx, t, WithNoGateLocking(true)) case *err: queuedOp = "error" writeErr = e.writeError(qe.ctx, t, WithNoGateLocking(true)) default: // Have no idea what this is and shouldn't have gotten in here to // begin with, so just continue, and log it writeErr = fmt.Errorf("unknown event type %T", t) } if writeErr != nil { totalErrs = errors.Join(fmt.Errorf("in %s, error sending queued %s event: %w", op, queuedOp, writeErr)) } } e.gatedQueue = nil return totalErrs } // StandardLogger will create *log.Logger that will emit events through this // Logger. This allows packages that require the stdlib log to emit events // instead. func (e *Eventer) StandardLogger(ctx context.Context, loggerName string, typ Type) (*log.Logger, error) { const op = "event.(Eventer).StandardLogger" if e == nil { return nil, fmt.Errorf("%s: nil eventer: %w", op, ErrInvalidParameter) } if ctx == nil { return nil, fmt.Errorf("%s: missing context: %w", op, ErrInvalidParameter) } if typ == "" { return nil, fmt.Errorf("%s: missing type: %w", op, ErrInvalidParameter) } w, err := e.StandardWriter(ctx, typ) if err != nil { return nil, fmt.Errorf("%s: %w", op, err) } return log.New(w, loggerName, 0), nil } // StandardWriter will create an io.Writer that will emit events through this // io.Writer. func (e *Eventer) StandardWriter(ctx context.Context, typ Type) (io.Writer, error) { const op = "event.(Eventer).StandardErrorWriter" if e == nil { return nil, fmt.Errorf("%s: nil eventer: %w", op, ErrInvalidParameter) } if ctx == nil { return nil, fmt.Errorf("%s: missing context: %w", op, ErrInvalidParameter) } if typ == "" { return nil, fmt.Errorf("%s: missing type: %w", op, ErrInvalidParameter) } if err := typ.Validate(); err != nil { return nil, fmt.Errorf("%s: %w", op, err) } ctx, err := NewEventerContext(ctx, e) if err != nil { return nil, fmt.Errorf("%s: %w", op, err) } return &logAdapter{ ctxWithEventer: ctx, e: e, emitEventType: typ, }, nil } type logAdapter struct { ctxWithEventer context.Context e *Eventer emitEventType Type } // Write satisfies the io.Writer interface and will take the data, infer the // type of event and then emit an event. func (s *logAdapter) Write(data []byte) (int, error) { const op = "event.(stdlogAdapter).Write" if s == nil { return 0, fmt.Errorf("%s: nil log adapter: %w", op, ErrInvalidParameter) } var caller Op pc, _, _, ok := runtime.Caller(1) details := runtime.FuncForPC(pc) if ok && details != nil { caller = Op(details.Name()) } else { caller = "unknown operation" } str := string(bytes.TrimRight(data, " \t\n")) switch s.emitEventType { case ErrorType, SystemType: if err := s.send(s.emitEventType, caller, str); err != nil { return 0, fmt.Errorf("%s: %w", op, err) } default: t, str := s.pickType(str) if err := s.send(t, caller, str); err != nil { return 0, fmt.Errorf("%s: %w", op, err) } } return len(data), nil } func (s *logAdapter) send(typ Type, caller Op, str string) error { const op = "events.(stdlogAdapter).send" if typ == "" { return fmt.Errorf("%s: type is missing: %w", op, ErrInvalidParameter) } if caller == "" { return fmt.Errorf("%s: missing caller: %w", op, ErrInvalidParameter) } switch typ { case ErrorType: WriteError(s.ctxWithEventer, caller, errors.New(str)) return nil case SystemType: WriteSysEvent(s.ctxWithEventer, caller, str) return nil default: return fmt.Errorf("%s: unsupported event type %s: %w", op, typ, ErrInvalidParameter) } } func (s *logAdapter) pickType(str string) (Type, string) { switch { case strings.HasPrefix(str, "[DEBUG]"): return SystemType, strings.TrimSpace(str[7:]) case strings.HasPrefix(str, "[TRACE]"): return SystemType, strings.TrimSpace(str[7:]) case strings.HasPrefix(str, "[INFO]"): return SystemType, strings.TrimSpace(str[6:]) case strings.HasPrefix(str, "[WARN]"): return SystemType, strings.TrimSpace(str[6:]) case strings.HasPrefix(str, "[ERROR]"): return ErrorType, strings.TrimSpace(str[7:]) case strings.HasPrefix(str, "[ERR]"): return ErrorType, strings.TrimSpace(str[5:]) default: return SystemType, str } } func isNil(i any) bool { if i == nil { return true } switch reflect.TypeOf(i).Kind() { case reflect.Ptr, reflect.Map, reflect.Array, reflect.Chan, reflect.Slice: return reflect.ValueOf(i).IsNil() } return false }