From b0394eff3202f884423603bdc8b92c31b7a6315b Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Wed, 24 Aug 2022 14:18:41 -0600 Subject: [PATCH 1/3] caddyevents: New events app Notably, this includes core changes to the caddy.Context. We now keep a lineage of modules provisioned by each context, and store the loaded modules as Module types, not empty interfaces. --- context.go | 68 +++- modules/caddyevent/app.go | 202 ----------- modules/caddyevent/event.go | 140 -------- modules/caddyevent/listener.go | 46 --- modules/caddyevents/app.go | 318 ++++++++++++++++++ modules/caddyevents/eventsconfig/caddyfile.go | 88 +++++ modules/caddyevents/exec.go | 88 +++++ modules/caddyhttp/app.go | 8 + modules/caddyhttp/reverseproxy/event.go | 56 --- .../caddyhttp/reverseproxy/healthchecks.go | 4 +- .../caddyhttp/reverseproxy/reverseproxy.go | 15 +- modules/caddyhttp/server.go | 6 + modules/caddytls/event.go | 93 ----- modules/caddytls/tls.go | 32 +- modules/standard/imports.go | 3 +- 15 files changed, 595 insertions(+), 572 deletions(-) delete mode 100644 modules/caddyevent/app.go delete mode 100644 modules/caddyevent/event.go delete mode 100644 modules/caddyevent/listener.go create mode 100644 modules/caddyevents/app.go create mode 100644 modules/caddyevents/eventsconfig/caddyfile.go create mode 100644 modules/caddyevents/exec.go delete mode 100644 modules/caddyhttp/reverseproxy/event.go delete mode 100644 modules/caddytls/event.go diff --git a/context.go b/context.go index 2a6f514214a..a2506a75b2a 100644 --- a/context.go +++ b/context.go @@ -37,9 +37,10 @@ import ( // not actually need to do this). type Context struct { context.Context - moduleInstances map[string][]interface{} + moduleInstances map[string][]Module cfg *Config cleanupFuncs []func() + ancestry []Module } // NewContext provides a new context derived from the given @@ -51,7 +52,7 @@ type Context struct { // modules which are loaded will be properly unloaded. // See standard library context package's documentation. func NewContext(ctx Context) (Context, context.CancelFunc) { - newCtx := Context{moduleInstances: make(map[string][]interface{}), cfg: ctx.cfg} + newCtx := Context{moduleInstances: make(map[string][]Module), cfg: ctx.cfg} c, cancel := context.WithCancel(ctx.Context) wrappedCancel := func() { cancel() @@ -90,15 +91,15 @@ func (ctx *Context) OnCancel(f func()) { // ModuleMap may be used in place of map[string]json.RawMessage. The return value's // underlying type mirrors the input field's type: // -// json.RawMessage => interface{} -// []json.RawMessage => []interface{} -// [][]json.RawMessage => [][]interface{} -// map[string]json.RawMessage => map[string]interface{} -// []map[string]json.RawMessage => []map[string]interface{} +// json.RawMessage => interface{} +// []json.RawMessage => []interface{} +// [][]json.RawMessage => [][]interface{} +// map[string]json.RawMessage => map[string]interface{} +// []map[string]json.RawMessage => []map[string]interface{} // // The field must have a "caddy" struct tag in this format: // -// caddy:"key1=val1 key2=val2" +// caddy:"key1=val1 key2=val2" // // To load modules, a "namespace" key is required. For example, to load modules // in the "http.handlers" namespace, you'd put: `namespace=http.handlers` in the @@ -115,7 +116,7 @@ func (ctx *Context) OnCancel(f func()) { // meaning the key containing the module's name that is defined inline with the module // itself. You must specify the inline key in a struct tag, along with the namespace: // -// caddy:"namespace=http.handlers inline_key=handler" +// caddy:"namespace=http.handlers inline_key=handler" // // This will look for a key/value pair like `"handler": "..."` in the json.RawMessage // in order to know the module name. @@ -301,17 +302,17 @@ func (ctx Context) loadModuleMap(namespace string, val reflect.Value) (map[strin // like from embedded scripts, etc. func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (interface{}, error) { modulesMu.RLock() - mod, ok := modules[id] + modInfo, ok := modules[id] modulesMu.RUnlock() if !ok { return nil, fmt.Errorf("unknown module: %s", id) } - if mod.New == nil { - return nil, fmt.Errorf("module '%s' has no constructor", mod.ID) + if modInfo.New == nil { + return nil, fmt.Errorf("module '%s' has no constructor", modInfo.ID) } - val := mod.New().(interface{}) + val := modInfo.New() // value must be a pointer for unmarshaling into concrete type, even if // the module's concrete type is a slice or map; New() *should* return @@ -327,7 +328,7 @@ func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (interface{ if len(rawMsg) > 0 { err := strictUnmarshalJSON(rawMsg, &val) if err != nil { - return nil, fmt.Errorf("decoding module config: %s: %v", mod, err) + return nil, fmt.Errorf("decoding module config: %s: %v", modInfo, err) } } @@ -340,6 +341,8 @@ func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (interface{ return nil, fmt.Errorf("module value cannot be null") } + ctx.ancestry = append(ctx.ancestry, val) + if prov, ok := val.(Provisioner); ok { err := prov.Provision(ctx) if err != nil { @@ -351,7 +354,7 @@ func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (interface{ err = fmt.Errorf("%v; additionally, cleanup: %v", err, err2) } } - return nil, fmt.Errorf("provision %s: %v", mod, err) + return nil, fmt.Errorf("provision %s: %v", modInfo, err) } } @@ -365,7 +368,7 @@ func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (interface{ err = fmt.Errorf("%v; additionally, cleanup: %v", err, err2) } } - return nil, fmt.Errorf("%s: invalid configuration: %v", mod, err) + return nil, fmt.Errorf("%s: invalid configuration: %v", modInfo, err) } } @@ -439,8 +442,10 @@ func (ctx Context) Storage() certmagic.Storage { return ctx.cfg.storage } +// TODO: aw man, can I please change this? // Logger returns a logger that can be used by mod. func (ctx Context) Logger(mod Module) *zap.Logger { + // TODO: if mod is nil, use ctx.Module() instead... if ctx.cfg == nil { // often the case in tests; just use a dev logger l, err := zap.NewDevelopment() @@ -451,3 +456,34 @@ func (ctx Context) Logger(mod Module) *zap.Logger { } return ctx.cfg.Logging.Logger(mod) } + +// TODO: use this +// // Logger returns a logger that can be used by the current module. +// func (ctx Context) Log() *zap.Logger { +// if ctx.cfg == nil { +// // often the case in tests; just use a dev logger +// l, err := zap.NewDevelopment() +// if err != nil { +// panic("config missing, unable to create dev logger: " + err.Error()) +// } +// return l +// } +// return ctx.cfg.Logging.Logger(ctx.Module()) +// } + +// Modules returns the lineage of modules that this context provisioned, +// with the most recent/current module being last in the list. +func (ctx Context) Modules() []Module { + mods := make([]Module, len(ctx.ancestry)) + copy(mods, ctx.ancestry) + return mods +} + +// Module returns the current module, or the most recent one +// provisioned by the context. +func (ctx Context) Module() Module { + if len(ctx.ancestry) == 0 { + return nil + } + return ctx.ancestry[len(ctx.ancestry)-1] +} diff --git a/modules/caddyevent/app.go b/modules/caddyevent/app.go deleted file mode 100644 index 59e4c18f251..00000000000 --- a/modules/caddyevent/app.go +++ /dev/null @@ -1,202 +0,0 @@ -// Copyright 2015 Matthew Holt and The Caddy Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package caddyevent - -import ( - "encoding/json" - "fmt" - "sort" - - "github.com/caddyserver/caddy/v2" - "go.uber.org/zap" -) - -func init() { - caddy.RegisterModule(EventApp{}) -} - -// EventApp is a global event system. -type EventApp struct { - // Registers each of these event subscribers - SubscribersRaw []json.RawMessage `json:"subscribers,omitempty" caddy:"namespace=event.subscribers inline_key=subscriber"` - - listeners map[caddy.ModuleID]map[Priority][]ListenerFunc - optimized map[caddy.ModuleID][]ListenerFunc - ready bool - logger *zap.Logger -} - -// CaddyModule returns the Caddy module information. -func (EventApp) CaddyModule() caddy.ModuleInfo { - return caddy.ModuleInfo{ - ID: "event", - New: func() caddy.Module { return new(EventApp) }, - } -} - -// Provision sets up the app. -func (app *EventApp) Provision(ctx caddy.Context) error { - app.listeners = make(map[caddy.ModuleID]map[Priority][]ListenerFunc) - app.logger = ctx.Logger(app) - - // register all the configured subscribers - if app.SubscribersRaw != nil { - subscribersIface, err := ctx.LoadModule(app, "SubscribersRaw") - if err != nil { - return fmt.Errorf("loading event subscriber modules: %v", err) - } - for _, subscriber := range subscribersIface.([]Subscriber) { - app.RegisterSubscriber(subscriber) - } - } - - return nil -} - -// Validate ensures the app's configuration is valid. -func (app *EventApp) Validate() error { - return nil -} - -// Start runs the app. -func (app *EventApp) Start() error { - // optimize the event listeners to order them by priority. - app.optimizeListeners() - - // stop new listeners from being added, - // and allow events to be dispatched. - app.ready = true - - return nil -} - -// Stop gracefully shuts down the app. -func (app *EventApp) Stop() error { - return nil -} - -// RegisterSubscriber registers all the listeners from a subscriber. -// Modules may register themselves as subscribers during their Provision -// phase. Subscribers cannot be registered after the config is running. -func (app *EventApp) RegisterSubscriber(subscriber Subscriber) { - for eventID, entry := range subscriber.SubscribedEvents() { - app.RegisterListener(eventID, entry) - } -} - -// RegisterListener registers a single event listener. -// Modules may register listeners during their Provision phase. -// Listeners cannot be registered after the config is running. -func (app *EventApp) RegisterListener(eventID caddy.ModuleID, entry ListenerEntry) { - // if the app is already running, we don't allow adding new listeners. - if app.ready { - // TODO: Panic or something? - return - } - - if app.listeners[eventID] == nil { - app.listeners[eventID] = make(map[Priority][]ListenerFunc) - } - - // There may be more than one listener with the same priority, - // for the same event, so we have an array of listeners for - // each priority level. Listeners at the same priority level - // will be in the order they are registered, which will not - // have a guaranteed order because Caddy modules may be loaded - // in an arbitrary order. - app.listeners[eventID][entry.Priority] = append( - app.listeners[eventID][entry.Priority], - entry.Listener, - ) - - app.logger.Debug("registered listener", - zap.String("event", eventID.Name()), - zap.Int("priority", int(entry.Priority)), - ) -} - -// Dispatch passes the event through the configured listeners synchronously. -func (app *EventApp) Dispatch(event Event) error { - // if the app is not running, we don't allow dispatching events. - if !app.ready { - return fmt.Errorf("Cannot dispatch events until after the app is running") - } - - // find the listeners for this event - listeners, ok := app.optimized[event.ID()] - if !ok { - return nil - } - - app.logger.Debug("dispatching", zap.String("event", event.ID().Name())) - - for _, listener := range listeners { - // listeners may mark the event to stop subsequent - // listeners from running on this event. - if event.IsPropagationStopped() { - app.logger.Debug("propagation stopped", zap.String("event", event.ID().Name())) - break - } - - // run the listener. - err := listener(event) - if err != nil { - app.logger.Error("listener error", - zap.String("event", event.ID().Name()), - zap.Error(err), - ) - return err - } - } - - return nil -} - -// AsyncDispatch passes the event through the configured listeners asynchronously. -func (app *EventApp) AsyncDispatch(event Event) { - go func(event Event) { - _ = app.Dispatch(event) - }(event) -} - -// optimizeListeners orders the listeners by priority. -func (app *EventApp) optimizeListeners() { - app.optimized = make(map[caddy.ModuleID][]ListenerFunc) - for eventID, priorities := range app.listeners { - app.optimized[eventID] = []ListenerFunc{} - - // sort the priorities (highest priority value first) - keys := make([]int, 0) - for k := range priorities { - keys = append(keys, int(k)) - } - sort.Sort(sort.Reverse(sort.IntSlice(keys))) - - // add all the listeners in order of priority - for _, k := range keys { - app.optimized[eventID] = append(app.optimized[eventID], priorities[Priority(k)]...) - } - } - - // we no longer need this map once we're running - app.listeners = nil -} - -// Interface guards -var ( - _ caddy.App = (*EventApp)(nil) - _ caddy.Provisioner = (*EventApp)(nil) - _ caddy.Validator = (*EventApp)(nil) -) diff --git a/modules/caddyevent/event.go b/modules/caddyevent/event.go deleted file mode 100644 index 6255e5e6b66..00000000000 --- a/modules/caddyevent/event.go +++ /dev/null @@ -1,140 +0,0 @@ -// Copyright 2015 Matthew Holt and The Caddy Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package caddyevent - -import ( - "github.com/caddyserver/caddy/v2" -) - -// DataMap is a key-value pair map. -type DataMap map[string]interface{} - -// Event interface -type Event interface { - // Get the module ID of the event. - ID() caddy.ModuleID - - // Get an item from the data map by key. - Get(key string) interface{} - - // Set an item in the data map by key. - Set(key string, val interface{}) - - // Get the entire data map. - Data() DataMap - - // Replace the entire data map. - SetData(DataMap) - - // StopPropagation sets the event to no longer propagate - // to subsequent event listeners. - StopPropagation(bool) - - // IsPropagationStopped returns whether the event is set - // to no longer propagate. In other words, this is - // whether the event dispatcher should stop handling - // this event and skip passing it to any subsequent - // event listeners. - IsPropagationStopped() bool -} - -// GenericEvent a generic event, which can be used -// with composition to provide baseline functionality -// for custom events. -type GenericEvent struct { - // The module ID of the event. - id caddy.ModuleID - - // Key-value data pairs of contextual information. - data DataMap - - // Whether the event handling should be aborted, - // and no event handlers should be called after - // this point. Allows for "middleware chain" type - // of functionality. - propagationStopped bool -} - -// NewGeneric creates a generic event instance. -func NewGeneric(id caddy.ModuleID, data DataMap) *GenericEvent { - if data == nil { - data = make(DataMap) - } - - return &GenericEvent{ - id: id, - data: data, - } -} - -// SetID sets the module ID of the event. -func (e *GenericEvent) SetID(id caddy.ModuleID) { - e.id = id -} - -// Get the module ID of the event. -func (e *GenericEvent) ID() caddy.ModuleID { - return e.id -} - -// Get an item from the data map by key. -func (e *GenericEvent) Get(key string) interface{} { - if v, ok := e.data[key]; ok { - return v - } - - return nil -} - -// Set an item in the data map by key. -func (e *GenericEvent) Set(key string, val interface{}) { - if e.data == nil { - e.data = make(DataMap) - } - - e.data[key] = val -} - -// Data returns the entire data map. -func (e *GenericEvent) Data() DataMap { - return e.data -} - -// SetData overwrites the entire data map. -func (e *GenericEvent) SetData(data DataMap) { - if data != nil { - e.data = data - } -} - -// StopPropagation sets the event to no longer propagate -// to subsequent event listeners. -func (e *GenericEvent) StopPropagation(abort bool) { - e.propagationStopped = abort -} - -// IsPropagationStopped returns whether the event is set -// to no longer propagate. In other words, this is -// whether the event dispatcher should stop handling -// this event and skip passing it to any subsequent -// event listeners. -func (e *GenericEvent) IsPropagationStopped() bool { - return e.propagationStopped -} - -// Interface guards -var ( - _ Event = (*GenericEvent)(nil) -) diff --git a/modules/caddyevent/listener.go b/modules/caddyevent/listener.go deleted file mode 100644 index 8b29e897608..00000000000 --- a/modules/caddyevent/listener.go +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright 2015 Matthew Holt and The Caddy Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package caddyevent - -import ( - "github.com/caddyserver/caddy/v2" -) - -// ListenerFunc is a function that can handle a dispatched event. -type ListenerFunc func(e Event) error - -// Handle runs the event listener on the given event. -func (fn ListenerFunc) Handle(e Event) error { - return fn(e) -} - -// Priority is a factor by which event listeners can be sorted. -type Priority int - -// ListenerEntry is a wrapper to allow associating a listener function -// to a priority factor when registering event subscribers and listeners. -type ListenerEntry struct { - Listener ListenerFunc - Priority Priority -} - -// Subscriber defines an interface for modules that wish -// to subscribe to dispatched events. -type Subscriber interface { - // SubscribedEvents returns a map of event IDs that - // this subscriber can handle, to the function that - // handles the event. - SubscribedEvents() map[caddy.ModuleID]ListenerEntry -} diff --git a/modules/caddyevents/app.go b/modules/caddyevents/app.go new file mode 100644 index 00000000000..4d3d455bc6f --- /dev/null +++ b/modules/caddyevents/app.go @@ -0,0 +1,318 @@ +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package caddyevents + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "strings" + "time" + + "github.com/caddyserver/caddy/v2" + "github.com/google/uuid" + "go.uber.org/zap" +) + +func init() { + caddy.RegisterModule(App{}) +} + +// App implements a global eventing system within Caddy. +// Modules can emit and subscribe to events, providing +// hooks into deep parts of the code base that aren't +// otherwise accessible. Events provide information about +// what and when things are happening, and this facility +// allows handlers to take action when events occur, +// add information to the event's metadata, and even +// control program flow in some cases. +// +// Events are propagated in a DOM-like fashion. An event +// event emitted from module `a.b.c` (the "origin") will +// first invoke handlers listening to `a.b.c`, then `a.b`, +// then `a`, then those listening regardless of origin. +// If a handler returns the special error Aborted, then +// propagation immediately stops and the event is marked +// as aborted. Emitters may optionally choose to adjust +// program flow based on an abort. +// +// Modules can subscribe to events by origin and/or name. +// A handler is invoked only if it is subscribed to the +// event by name and origin. Subscriptions should be +// registered during the provisioning phase, before apps +// are started. +// +// Event handlers are fired synchronously as part of the +// regular flow of the program. This allows event handlers +// to control the flow of the program if the origin permits +// it and also allows handlers to convey new information +// back into the origin module before it continues. +// In essence, event handlers are similar to HTTP +// middleware handlers. +// +// Event bindings/subscribers are unordered; i.e. +// event handlers are invoked in an arbitrary order. +// Event handlers should not rely on the logic of other +// handlers to succeed. +type App struct { + // Subscriptions bind handlers to one or more events + // either globally or scoped to specific modules or module + // namespaces. + Subscriptions []*Subscription `json:"subscriptions,omitempty"` + + // Map of event name to map of module ID/namespace to handlers + subscriptions map[string]map[caddy.ModuleID][]Handler + + logger *zap.Logger + started bool +} + +// Subscription represents binding of one or more handlers to +// one or more events. +type Subscription struct { + // The name(s) of the event(s) to bind to. Default: all events. + Events []string `json:"events,omitempty"` + + // The ID or namespace of the module(s) from which events + // originate to listen to for events. Default: all modules. + // Events propagate up, so events emitted by module "a.b.c" + // will also trigger the event for "a.b" and "a". Thus, to + // receive all events from "a.b.c" and "a.b.d", for example, + // one can subscribe to either "a.b" or all of "a" entirely. + Modules []caddy.ModuleID `json:"modules,omitempty"` + + // The event handler modules. These implement the actual + // behavior to invoke when an event occurs. At least one + // handler is required. + HandlersRaw []json.RawMessage `json:"handlers,omitempty" caddy:"namespace=events.handlers inline_key=handler"` + + // The decoded handlers; Go code that is subscribing to + // an event should set this field directly; HandlersRaw + // is meant for JSON configuration to fill out this field. + Handlers []Handler `json:"-"` +} + +// CaddyModule returns the Caddy module information. +func (App) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "events", + New: func() caddy.Module { return new(App) }, + } +} + +// Provision sets up the app. +func (app *App) Provision(ctx caddy.Context) error { + app.logger = ctx.Logger(app) + app.subscriptions = make(map[string]map[caddy.ModuleID][]Handler) + + for _, sub := range app.Subscriptions { + if sub.HandlersRaw != nil { + handlersIface, err := ctx.LoadModule(sub, "HandlersRaw") + if err != nil { + return fmt.Errorf("loading event subscriber modules: %v", err) + } + for _, h := range handlersIface.([]interface{}) { + sub.Handlers = append(sub.Handlers, h.(Handler)) + } + if len(sub.Handlers) == 0 { + // pointless to bind without any handlers + return fmt.Errorf("no handlers defined") + } + } + } + + return nil +} + +// Start runs the app. +func (app *App) Start() error { + for _, sub := range app.Subscriptions { + if err := app.Subscribe(sub); err != nil { + return err + } + } + + app.started = true + + return nil +} + +// Stop gracefully shuts down the app. +func (app *App) Stop() error { + return nil +} + +// Subscribe binds one or more event handlers to one or more events +// according to the subscription s. For now, subscriptions can only +// be created during the provision phase; new bindings cannot be +// created after the events app has started. +func (app *App) Subscribe(s *Subscription) error { + if app.started { + return fmt.Errorf("events already started; new subscriptions closed") + } + + // handle special case of catch-alls (omission of event name or module space implies all) + if len(s.Events) == 0 { + s.Events = []string{""} + } + if len(s.Modules) == 0 { + s.Modules = []caddy.ModuleID{""} + } + + for _, eventName := range s.Events { + if app.subscriptions[eventName] == nil { + app.subscriptions[eventName] = make(map[caddy.ModuleID][]Handler) + } + for _, originModule := range s.Modules { + app.subscriptions[eventName][originModule] = append(app.subscriptions[eventName][originModule], s.Handlers...) + } + } + + return nil +} + +// On is syntactic sugar for Subscribe() that binds a single handler +// to a single event from any module. If the eventName is empty string, +// it counts for all events. +func (app *App) On(eventName string, handler Handler) error { + return app.Subscribe(&Subscription{ + Events: []string{eventName}, + Handlers: []Handler{handler}, + }) +} + +// Emit creates and dispatches an event named eventName to all relevant handlers with +// the metadata data. Events are emitted and propagated synchronously. The returned Event +// value will have any additional information from the invoked handlers. +func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]interface{}) Event { + id, err := uuid.NewRandom() + if err != nil { + app.logger.Error("failed generating new event ID", zap.Error(err)) + } + + eventName = strings.ToLower(eventName) + + // TODO: make pointer? + e := Event{ + id: id, + ts: time.Now(), + name: eventName, + origin: ctx.Module(), + data: data, + } + + // add event info to replacer, make sure it's in the context + repl, ok := ctx.Context.Value(caddy.ReplacerCtxKey).(*caddy.Replacer) + if !ok { + repl = caddy.NewReplacer() + ctx.Context = context.WithValue(ctx.Context, caddy.ReplacerCtxKey, repl) + } + repl.Set("event", e) + repl.Set("event.id", e.id) + repl.Set("event.timestamp", e.ts) + repl.Set("event.name", eventName) + repl.Set("event.module", e.origin.CaddyModule().ID) + repl.Set("event.data", e.data) + repl.Map(func(key string) (interface{}, bool) { + if !strings.HasPrefix(key, "event.data.") { + return nil, false + } + key = strings.TrimPrefix(key, "event.data.") + if val, ok := data[key]; ok { + return val, true + } + return nil, false + }) + + // TODO: log that event was emitted... hm, maybe disable our logger by default? or only emit logs if any subscriptions are configured? or maybe debug level? hmm + + // invoke handlers bound to the event by name and also all events; this for loop + // iterates twice at most: once for the event name, once for "" (all events) + for { + moduleID := e.origin.CaddyModule().ID + + // implement propagation up the module tree (i.e. start with "a.b.c" then "a.b" then "a" then "") + for { + if app.subscriptions[eventName] == nil { + break // shortcut if event not bound at all + } + + for _, handler := range app.subscriptions[eventName][moduleID] { + // TODO: maybe log handler invocations instead of all event emissions? + err := handler.Handle(ctx, e) + if errors.Is(err, ErrAborted) { + // TODO: Figure out proper way to implement this + e.Aborted = err + return e + } + if err != nil { + app.logger.Error("handler error", zap.Error(err)) + } + } + + if moduleID == "" { + break + } + lastDot := strings.LastIndex(string(moduleID), ".") + if lastDot < 0 { + moduleID = "" // include handlers bound to events regardless of module + } else { + moduleID = moduleID[:lastDot] + } + } + + // include handlers listening to all events + if eventName == "" { + break + } + eventName = "" + } + + return e +} + +// Event represents something that has happened or is happening. +type Event struct { + id uuid.UUID + ts time.Time + name string + origin caddy.Module + data map[string]interface{} + + // If non-nil, the event has been aborted, meaning + // propagation has stopped to other handlers and + // the code should stop what it was doing. Emitters + // may choose to use this as a signal to adjust their + // code path appropriately. + Aborted error +} + +// ErrAborted cancels an event. +var ErrAborted = errors.New("event aborted") + +// Handler is a type that can handle events. +type Handler interface { + Handle(context.Context, Event) error +} + +// // HandlerFunc +// type HandlerFunc func(context.Context, Event) error + +// Interface guards +var ( + _ caddy.App = (*App)(nil) + _ caddy.Provisioner = (*App)(nil) +) diff --git a/modules/caddyevents/eventsconfig/caddyfile.go b/modules/caddyevents/eventsconfig/caddyfile.go new file mode 100644 index 00000000000..6b1347d6cba --- /dev/null +++ b/modules/caddyevents/eventsconfig/caddyfile.go @@ -0,0 +1,88 @@ +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package eventsconfig is for configuring caddyevents.App with the +// Caddyfile. This code can't be in the caddyevents package because +// the httpcaddyfile package imports caddyhttp, which imports +// caddyevents: hence, it creates an import cycle. +package eventsconfig + +import ( + "encoding/json" + + "github.com/caddyserver/caddy/v2/caddyconfig" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" + "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" + "github.com/caddyserver/caddy/v2/modules/caddyevents" +) + +func init() { + httpcaddyfile.RegisterGlobalOption("events", parseApp) +} + +// parseApp configures the "events" global option from Caddyfile to set up the events app. +// Syntax: +// +// events { +// on +// } +// +// If is *, then it will bind to all events. +func parseApp(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) { + app := new(caddyevents.App) + + // consume the option name + if !d.Next() { + return nil, d.ArgErr() + } + + // handle the block + for d.NextBlock(0) { + switch d.Val() { + case "on": + if !d.NextArg() { + return nil, d.ArgErr() + } + eventName := d.Val() + if eventName == "*" { + eventName = "" + } + + if !d.NextArg() { + return nil, d.ArgErr() + } + handlerName := d.Val() + modID := "events.handlers." + handlerName + unm, err := caddyfile.UnmarshalModule(d, modID) + if err != nil { + return nil, err + } + + app.Subscriptions = append(app.Subscriptions, &caddyevents.Subscription{ + Events: []string{eventName}, + HandlersRaw: []json.RawMessage{ + caddyconfig.JSONModuleObject(unm, "handler", handlerName, nil), + }, + }) + + default: + return nil, d.ArgErr() + } + } + + return httpcaddyfile.App{ + Name: "events", + Value: caddyconfig.JSON(app, nil), + }, nil +} diff --git a/modules/caddyevents/exec.go b/modules/caddyevents/exec.go new file mode 100644 index 00000000000..d5962e73d5b --- /dev/null +++ b/modules/caddyevents/exec.go @@ -0,0 +1,88 @@ +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package caddyevents + +import ( + "context" + "os" + "os/exec" + + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" + "go.uber.org/zap" +) + +func init() { + caddy.RegisterModule(ExecHandler{}) +} + +// ExecHandler implements an event handler that runs a command/program. +type ExecHandler struct { + Command string `json:"command,omitempty"` + Args []string `json:"args,omitempty"` + Dir string `json:"dir,omitempty"` + Timeout caddy.Duration `json:"timeout,omitempty"` + + logger *zap.Logger +} + +// CaddyModule returns the Caddy module information. +func (ExecHandler) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "events.handlers.exec", + New: func() caddy.Module { return new(ExecHandler) }, + } +} + +// Provision sets up the module. +func (eh *ExecHandler) Provision(ctx caddy.Context) error { + eh.logger = ctx.Logger(eh) + return nil +} + +func (eh *ExecHandler) Handle(ctx context.Context, e Event) error { + repl := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer) + + // expand placeholders in command args; + // notably, WE DO NOT EXPAND PLACEHOLDERS + // IN THE COMMAND ITSELF for safety reasons + expandedArgs := make([]string, len(eh.Args)) + for i := range eh.Args { + expandedArgs[i] = repl.ReplaceAll(eh.Args[i], "") + } + + cmd := exec.Command(eh.Command, expandedArgs...) + cmd.Dir = eh.Dir + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} + +func (eh *ExecHandler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + for d.Next() { + if !d.NextArg() { + return d.ArgErr() + } + eh.Command = d.Val() + eh.Args = d.RemainingArgs() + } + return nil +} + +// Interface guards +var ( + _ caddyfile.Unmarshaler = (*ExecHandler)(nil) + _ caddy.Provisioner = (*ExecHandler)(nil) +) diff --git a/modules/caddyhttp/app.go b/modules/caddyhttp/app.go index 1894a975ce4..603108aa62f 100644 --- a/modules/caddyhttp/app.go +++ b/modules/caddyhttp/app.go @@ -23,6 +23,7 @@ import ( "time" "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/modules/caddyevents" "github.com/caddyserver/caddy/v2/modules/caddytls" "github.com/lucas-clemente/quic-go/http3" "go.uber.org/zap" @@ -146,6 +147,11 @@ func (app *App) Provision(ctx caddy.Context) error { app.ctx = ctx app.logger = ctx.Logger(app) + eventsAppIface, err := ctx.App("events") + if err != nil { + return fmt.Errorf("getting events app: %v", err) + } + repl := caddy.NewReplacer() // this provisions the matchers for each route, @@ -160,6 +166,8 @@ func (app *App) Provision(ctx caddy.Context) error { for srvName, srv := range app.Servers { srv.name = srvName srv.tlsApp = app.tlsApp + srv.events = eventsAppIface.(*caddyevents.App) + srv.ctx = ctx srv.logger = app.logger.Named("log") srv.errorLogger = app.logger.Named("log.error") diff --git a/modules/caddyhttp/reverseproxy/event.go b/modules/caddyhttp/reverseproxy/event.go deleted file mode 100644 index 2ab39fdb308..00000000000 --- a/modules/caddyhttp/reverseproxy/event.go +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright 2015 Matthew Holt and The Caddy Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package reverseproxy - -import ( - "github.com/caddyserver/caddy/v2" - "github.com/caddyserver/caddy/v2/modules/caddyevent" -) - -// ActiveUnhealthyEvent is dispatched when an upstream -// became unhealthy via active health checks, when it was -// previously healthy. -type ActiveUnhealthyEvent struct { - caddyevent.GenericEvent -} - -func NewActiveUnhealthyEvent(hostAddr string) *ActiveUnhealthyEvent { - event := new(ActiveUnhealthyEvent) - event.SetID(caddy.ModuleID("http.handlers.reverse_proxy.event.active_unhealthy")) - event.Set("host", hostAddr) - return event -} - -func (e ActiveUnhealthyEvent) GetHost() string { - return e.Get("host").(string) -} - -// ActiveUnhealthyEvent is dispatched when an upstream -// became healthy via active health checks, when it was -// previously unhealthy. -type ActiveHealthyEvent struct { - caddyevent.GenericEvent -} - -func NewActiveHealthyEvent(hostAddr string) *ActiveHealthyEvent { - event := new(ActiveHealthyEvent) - event.SetID(caddy.ModuleID("http.handlers.reverse_proxy.event.active_healthy")) - event.Set("host", hostAddr) - return event -} - -func (e ActiveHealthyEvent) GetHost() string { - return e.Get("host").(string) -} diff --git a/modules/caddyhttp/reverseproxy/healthchecks.go b/modules/caddyhttp/reverseproxy/healthchecks.go index c47b5bd6552..bbd81ccacda 100644 --- a/modules/caddyhttp/reverseproxy/healthchecks.go +++ b/modules/caddyhttp/reverseproxy/healthchecks.go @@ -287,7 +287,7 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, upstre markUnhealthy := func() { // dispatch an event that the host newly became unhealthy if upstream.setHealthy(false) { - h.event.AsyncDispatch(NewActiveUnhealthyEvent(hostAddr)) + h.events.Emit(h.ctx, "unhealthy", map[string]interface{}{"host": hostAddr}) } } @@ -353,7 +353,7 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, upstre // passed health check parameters, so mark as healthy if upstream.setHealthy(true) { h.HealthChecks.Active.logger.Info("host is up", zap.String("host", hostAddr)) - h.event.AsyncDispatch(NewActiveHealthyEvent(hostAddr)) + h.events.Emit(h.ctx, "healthy", map[string]interface{}{"host": hostAddr}) } return nil diff --git a/modules/caddyhttp/reverseproxy/reverseproxy.go b/modules/caddyhttp/reverseproxy/reverseproxy.go index 07eea31507e..91d2fdd352d 100644 --- a/modules/caddyhttp/reverseproxy/reverseproxy.go +++ b/modules/caddyhttp/reverseproxy/reverseproxy.go @@ -33,7 +33,7 @@ import ( "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" - "github.com/caddyserver/caddy/v2/modules/caddyevent" + "github.com/caddyserver/caddy/v2/modules/caddyevents" "github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp/headers" "github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite" @@ -180,7 +180,7 @@ type Handler struct { ctx caddy.Context logger *zap.Logger - event *caddyevent.EventApp + events *caddyevents.App } // CaddyModule returns the Caddy module information. @@ -193,14 +193,13 @@ func (Handler) CaddyModule() caddy.ModuleInfo { // Provision ensures that h is set up properly before use. func (h *Handler) Provision(ctx caddy.Context) error { - h.ctx = ctx - h.logger = ctx.Logger(h) - - eventAppIface, err := ctx.App("event") + eventAppIface, err := ctx.App("events") if err != nil { - return fmt.Errorf("getting event app: %v", err) + return fmt.Errorf("getting events app: %v", err) } - h.event = eventAppIface.(*caddyevent.EventApp) + h.events = eventAppIface.(*caddyevents.App) + h.ctx = ctx + h.logger = ctx.Logger(h) // verify SRV compatibility - TODO: LookupSRV deprecated; will be removed for i, v := range h.Upstreams { diff --git a/modules/caddyhttp/server.go b/modules/caddyhttp/server.go index a4a976f7ef7..5904a8a8b81 100644 --- a/modules/caddyhttp/server.go +++ b/modules/caddyhttp/server.go @@ -27,6 +27,7 @@ import ( "time" "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/modules/caddyevents" "github.com/caddyserver/caddy/v2/modules/caddytls" "github.com/caddyserver/certmagic" "github.com/lucas-clemente/quic-go/http3" @@ -134,15 +135,20 @@ type Server struct { listenerWrappers []caddy.ListenerWrapper tlsApp *caddytls.TLS + events *caddyevents.App logger *zap.Logger accessLogger *zap.Logger errorLogger *zap.Logger + ctx caddy.Context h3server *http3.Server } // ServeHTTP is the entry point for all HTTP requests. func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // TODO: consider emitting this asynchronously + s.events.Emit(s.ctx, "request", map[string]interface{}{"request": r}) + w.Header().Set("Server", "Caddy") if s.h3server != nil { diff --git a/modules/caddytls/event.go b/modules/caddytls/event.go deleted file mode 100644 index 7503cd25bcc..00000000000 --- a/modules/caddytls/event.go +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright 2015 Matthew Holt and The Caddy Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package caddytls - -import ( - "github.com/caddyserver/caddy/v2" - "github.com/caddyserver/caddy/v2/modules/caddyevent" - "github.com/caddyserver/certmagic" -) - -// onEvent translates certmagic events into caddy events -// then dispatches them asynchronously. -func (t *TLS) onEvent(event string, data interface{}) { - switch event { - // case "cached_managed_cert", "cached_unmanaged_cert": - // subjectNames, ok := data.([]string) - // if !ok { - // return - // } - // t.event.AsyncDispatch(caddyevent.NewGeneric( - // caddy.ModuleID("tls.event."+event), - // caddyevent.DataMap{"subjectNames": subjectNames}, - // )) - - // case "tls_handshake_started", "tls_handshake_completed": - // clientHello, ok := data.(*tls.ClientHelloInfo) - // if !ok { - // return - // } - // t.event.AsyncDispatch(NewHandshakeEvent(event, clientHello)) - - case "cert_obtained", "cert_renewed", "cert_revoked": - eventData, ok := data.(certmagic.CertificateEventData) - if !ok { - return - } - t.event.AsyncDispatch(NewCertEvent(event, eventData)) - } -} - -// CertEvent is dispatched when a certificate is obtained, renewed, or revoked. -type CertEvent struct { - caddyevent.GenericEvent -} - -func NewCertEvent(event string, data certmagic.CertificateEventData) *CertEvent { - certEvent := new(CertEvent) - certEvent.SetID(caddy.ModuleID("tls.event." + event)) - certEvent.Set("name", data.Name) - certEvent.Set("issuerKey", data.IssuerKey) - certEvent.Set("storageKey", data.StorageKey) - return certEvent -} - -func (e CertEvent) GetName() string { - return e.Get("name").(string) -} - -func (e CertEvent) GetIssuerKey() string { - return e.Get("issuerKey").(string) -} - -func (e CertEvent) GetStorageKey() string { - return e.Get("storageKey").(string) -} - -// HandshakeEvent is dispatched when a TLS handshake is started, or is completed. -// type HandshakeEvent struct { -// caddyevent.GenericEvent -// } - -// func NewHandshakeEvent(event string, clientHello *tls.ClientHelloInfo) *HandshakeEvent { -// handshakeEvent := new(HandshakeEvent) -// handshakeEvent.SetID(caddy.ModuleID("tls.event." + event)) -// handshakeEvent.Set("clientHello", clientHello) -// return handshakeEvent -// } - -// func (e CertEvent) GetClientHello() *tls.ClientHelloInfo { -// return e.Get("clientHello").(*tls.ClientHelloInfo) -// } diff --git a/modules/caddytls/tls.go b/modules/caddytls/tls.go index a310d194c5d..b66a9857746 100644 --- a/modules/caddytls/tls.go +++ b/modules/caddytls/tls.go @@ -25,7 +25,7 @@ import ( "time" "github.com/caddyserver/caddy/v2" - "github.com/caddyserver/caddy/v2/modules/caddyevent" + "github.com/caddyserver/caddy/v2/modules/caddyevents" "github.com/caddyserver/certmagic" "go.uber.org/zap" ) @@ -74,7 +74,7 @@ type TLS struct { storageCleanTicker *time.Ticker storageCleanStop chan struct{} logger *zap.Logger - event *caddyevent.EventApp + events *caddyevents.App } // CaddyModule returns the Caddy module information. @@ -87,16 +87,15 @@ func (TLS) CaddyModule() caddy.ModuleInfo { // Provision sets up the configuration for the TLS app. func (t *TLS) Provision(ctx caddy.Context) error { + eventsAppIface, err := ctx.App("events") + if err != nil { + return fmt.Errorf("getting events app: %v", err) + } + t.events = eventsAppIface.(*caddyevents.App) t.ctx = ctx t.logger = ctx.Logger(t) repl := caddy.NewReplacer() - eventAppIface, err := ctx.App("event") - if err != nil { - return fmt.Errorf("getting event app: %v", err) - } - t.event = eventAppIface.(*caddyevent.EventApp) - // set up a new certificate cache; this (re)loads all certificates cacheOpts := certmagic.CacheOptions{ GetConfigForCert: func(cert certmagic.Certificate) (*certmagic.Config, error) { @@ -523,6 +522,23 @@ func (t *TLS) storageCleanInterval() time.Duration { return defaultStorageCleanInterval } +// onEvent translates CertMagic events into Caddy events then dispatches them. +// TODO: enhance CertMagic's event features to better accommodate our needs +func (t *TLS) onEvent(eventName string, data interface{}) { + evtData := make(map[string]interface{}) + switch d := data.(type) { + case certmagic.CertificateEventData: + evtData["name"] = d.Name + evtData["issuer_key"] = d.IssuerKey + evtData["storage_key"] = d.StorageKey + case *tls.ClientHelloInfo: + evtData["client_hello"] = d + case []string: + evtData["subject_names"] = d + } + t.events.Emit(t.ctx, eventName, evtData) +} + // CertificateLoader is a type that can load certificates. // Certificates can optionally be associated with tags. type CertificateLoader interface { diff --git a/modules/standard/imports.go b/modules/standard/imports.go index 010742b597a..a9d0b396825 100644 --- a/modules/standard/imports.go +++ b/modules/standard/imports.go @@ -3,7 +3,8 @@ package standard import ( // standard Caddy modules _ "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" - _ "github.com/caddyserver/caddy/v2/modules/caddyevent" + _ "github.com/caddyserver/caddy/v2/modules/caddyevents" + _ "github.com/caddyserver/caddy/v2/modules/caddyevents/eventsconfig" _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/standard" _ "github.com/caddyserver/caddy/v2/modules/caddypki" _ "github.com/caddyserver/caddy/v2/modules/caddypki/acmeserver" From 62535b7dbeddc2a88dee23d7ac0b7237a61c85f7 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Thu, 25 Aug 2022 12:46:31 -0600 Subject: [PATCH 2/3] Don't emit request event Seems redundant to handlers. See if there's a real need for it. --- modules/caddyhttp/server.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/modules/caddyhttp/server.go b/modules/caddyhttp/server.go index 5904a8a8b81..61d9a4061a4 100644 --- a/modules/caddyhttp/server.go +++ b/modules/caddyhttp/server.go @@ -146,9 +146,6 @@ type Server struct { // ServeHTTP is the entry point for all HTTP requests. func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { - // TODO: consider emitting this asynchronously - s.events.Emit(s.ctx, "request", map[string]interface{}{"request": r}) - w.Header().Set("Server", "Caddy") if s.h3server != nil { From 0c5d9fe9da6e360def24f0a531955b0037debbc9 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Thu, 25 Aug 2022 16:33:40 -0600 Subject: [PATCH 3/3] Apply recommendations from code review --- modules/caddyevents/app.go | 56 ++++++++++++++++++++++++++++++++------ 1 file changed, 48 insertions(+), 8 deletions(-) diff --git a/modules/caddyevents/app.go b/modules/caddyevents/app.go index 4d3d455bc6f..761ea221894 100644 --- a/modules/caddyevents/app.go +++ b/modules/caddyevents/app.go @@ -222,22 +222,33 @@ func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]interf } repl.Set("event", e) repl.Set("event.id", e.id) - repl.Set("event.timestamp", e.ts) - repl.Set("event.name", eventName) + repl.Set("event.name", e.name) repl.Set("event.module", e.origin.CaddyModule().ID) repl.Set("event.data", e.data) repl.Map(func(key string) (interface{}, bool) { - if !strings.HasPrefix(key, "event.data.") { - return nil, false + switch key { + case "event.time": + return e.ts, true + case "event.time_unix": + return e.ts.UnixMilli(), true } - key = strings.TrimPrefix(key, "event.data.") - if val, ok := data[key]; ok { - return val, true + + if strings.HasPrefix(key, "event.data.") { + key = strings.TrimPrefix(key, "event.data.") + if val, ok := data[key]; ok { + return val, true + } } + return nil, false }) - // TODO: log that event was emitted... hm, maybe disable our logger by default? or only emit logs if any subscriptions are configured? or maybe debug level? hmm + app.logger.Debug("event", + zap.String("name", e.name), + zap.String("id", e.id.String()), + zap.String("origin", e.origin.CaddyModule().String()), + zap.Any("data", e.data), + ) // invoke handlers bound to the event by name and also all events; this for loop // iterates twice at most: once for the event name, once for "" (all events) @@ -300,6 +311,35 @@ type Event struct { Aborted error } +// CloudEvent exports event e as a structure that, when +// serialized as JSON, is compatible with the +// CloudEvents spec. +func (e Event) CloudEvent() CloudEvent { + dataJSON, _ := json.Marshal(e.data) + return CloudEvent{ + ID: e.id.String(), + Source: e.origin.CaddyModule().String(), + SpecVersion: "1.0", + Type: e.name, + Time: e.ts, + DataContentType: "application/json", + Data: dataJSON, + } +} + +// CloudEvent is a JSON-serializable structure that +// is compatible with the CloudEvents specification. +// See https://cloudevents.io. +type CloudEvent struct { + ID string `json:"id"` + Source string `json:"source"` + SpecVersion string `json:"specversion"` + Type string `json:"type"` + Time time.Time `json:"time"` + DataContentType string `json:"datacontenttype,omitempty"` + Data json.RawMessage `json:"data,omitempty"` +} + // ErrAborted cancels an event. var ErrAborted = errors.New("event aborted")