@marcel91200 / caddy Mirror · read-only / commits / 5a6b2f8d
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 12, 00:45 UTC

events: Refactor; move Event into core, so core can emit events (#6930)

* events: Refactor; move Event into core, so core can emit events Requires some slight trickery to invert dependencies. We can't have the caddy package import the caddyevents package, because caddyevents imports caddy. Interface to the rescue! Also add two new events, experimentally: started, and stopping. At the request of a sponsor. Also rename "Filesystems" to "FileSystems" to match Go convention (unrelated to events, was just bugging me when I noticed it). * Coupla bug fixes * lol whoops

Matt Holt committed Mar 29, 2025 at 08:15 UTC 5a6b2f8d1d4633622b551357f3cc9d27ec669d02
8 files changed +170 -122
caddy.go
+99 -5
@@ -81,13 +81,14 @@ type Config struct {
81 // associated value.
82 AppsRaw ModuleMap `json:"apps,omitempty" caddy:"namespace="`
83
84 - apps map[string]App
85 - storage certmagic.Storage
84 + apps map[string]App
85 + storage certmagic.Storage
86 + eventEmitter eventEmitter
87
88 cancelFunc context.CancelFunc
89
89 - // filesystems is a dict of filesystems that will later be loaded from and added to.
90 - filesystems FileSystems
90 + // fileSystems is a dict of fileSystems that will later be loaded from and added to.
91 + fileSystems FileSystems
92 }
93
94 // App is a thing that Caddy runs.
@@ -442,6 +443,10 @@ func run(newCfg *Config, start bool) (Context, error) {
443 }
444 globalMetrics.configSuccess.Set(1)
445 globalMetrics.configSuccessTime.SetToCurrentTime()
446 +
447 + // TODO: This event is experimental and subject to change.
448 + ctx.emitEvent("started", nil)
449 +
450 // now that the user's config is running, finish setting up anything else,
451 // such as remote admin endpoint, config loader, etc.
452 return ctx, finishSettingUp(ctx, ctx.cfg)
@@ -509,7 +514,7 @@ func provisionContext(newCfg *Config, replaceAdminServer bool) (Context, error)
514 }
515
516 // create the new filesystem map
512 - newCfg.filesystems = &filesystems.FilesystemMap{}
517 + newCfg.fileSystems = &filesystems.FileSystemMap{}
518
519 // prepare the new config for use
520 newCfg.apps = make(map[string]App)
@@ -696,6 +701,9 @@ func unsyncedStop(ctx Context) {
701 return
702 }
703
704 + // TODO: This event is experimental and subject to change.
705 + ctx.emitEvent("stopping", nil)
706 +
707 // stop each app
708 for name, a := range ctx.cfg.apps {
709 err := a.Stop()
@@ -1038,6 +1046,92 @@ func Version() (simple, full string) {
1046 return
1047 }
1048
1049 +// Event represents something that has happened or is happening.
1050 +// An Event value is not synchronized, so it should be copied if
1051 +// being used in goroutines.
1052 +//
1053 +// EXPERIMENTAL: Events are subject to change.
1054 +type Event struct {
1055 + // If non-nil, the event has been aborted, meaning
1056 + // propagation has stopped to other handlers and
1057 + // the code should stop what it was doing. Emitters
1058 + // may choose to use this as a signal to adjust their
1059 + // code path appropriately.
1060 + Aborted error
1061 +
1062 + // The data associated with the event. Usually the
1063 + // original emitter will be the only one to set or
1064 + // change these values, but the field is exported
1065 + // so handlers can have full access if needed.
1066 + // However, this map is not synchronized, so
1067 + // handlers must not use this map directly in new
1068 + // goroutines; instead, copy the map to use it in a
1069 + // goroutine. Data may be nil.
1070 + Data map[string]any
1071 +
1072 + id uuid.UUID
1073 + ts time.Time
1074 + name string
1075 + origin Module
1076 +}
1077 +
1078 +// NewEvent creates a new event, but does not emit the event. To emit an
1079 +// event, call Emit() on the current instance of the caddyevents app insteaad.
1080 +//
1081 +// EXPERIMENTAL: Subject to change.
1082 +func NewEvent(ctx Context, name string, data map[string]any) (Event, error) {
1083 + id, err := uuid.NewRandom()
1084 + if err != nil {
1085 + return Event{}, fmt.Errorf("generating new event ID: %v", err)
1086 + }
1087 + name = strings.ToLower(name)
1088 + return Event{
1089 + Data: data,
1090 + id: id,
1091 + ts: time.Now(),
1092 + name: name,
1093 + origin: ctx.Module(),
1094 + }, nil
1095 +}
1096 +
1097 +func (e Event) ID() uuid.UUID { return e.id }
1098 +func (e Event) Timestamp() time.Time { return e.ts }
1099 +func (e Event) Name() string { return e.name }
1100 +func (e Event) Origin() Module { return e.origin } // Returns the module that originated the event. May be nil, usually if caddy core emits the event.
1101 +
1102 +// CloudEvent exports event e as a structure that, when
1103 +// serialized as JSON, is compatible with the
1104 +// CloudEvents spec.
1105 +func (e Event) CloudEvent() CloudEvent {
1106 + dataJSON, _ := json.Marshal(e.Data)
1107 + return CloudEvent{
1108 + ID: e.id.String(),
1109 + Source: e.origin.CaddyModule().String(),
1110 + SpecVersion: "1.0",
1111 + Type: e.name,
1112 + Time: e.ts,
1113 + DataContentType: "application/json",
1114 + Data: dataJSON,
1115 + }
1116 +}
1117 +
1118 +// CloudEvent is a JSON-serializable structure that
1119 +// is compatible with the CloudEvents specification.
1120 +// See https://cloudevents.io.
1121 +// EXPERIMENTAL: Subject to change.
1122 +type CloudEvent struct {
1123 + ID string `json:"id"`
1124 + Source string `json:"source"`
1125 + SpecVersion string `json:"specversion"`
1126 + Type string `json:"type"`
1127 + Time time.Time `json:"time"`
1128 + DataContentType string `json:"datacontenttype,omitempty"`
1129 + Data json.RawMessage `json:"data,omitempty"`
1130 +}
1131 +
1132 +// ErrEventAborted cancels an event.
1133 +var ErrEventAborted = errors.New("event aborted")
1134 +
1135 // ActiveContext returns the currently-active context.
1136 // This function is experimental and might be changed
1137 // or removed in the future.
context.go
+28 -4
@@ -91,14 +91,14 @@ func (ctx *Context) OnCancel(f func()) {
91 ctx.cleanupFuncs = append(ctx.cleanupFuncs, f)
92 }
93
94 -// Filesystems returns a ref to the FilesystemMap.
94 +// FileSystems returns a ref to the FilesystemMap.
95 // EXPERIMENTAL: This API is subject to change.
96 -func (ctx *Context) Filesystems() FileSystems {
96 +func (ctx *Context) FileSystems() FileSystems {
97 // if no config is loaded, we use a default filesystemmap, which includes the osfs
98 if ctx.cfg == nil {
99 - return &filesystems.FilesystemMap{}
99 + return &filesystems.FileSystemMap{}
100 }
101 - return ctx.cfg.filesystems
101 + return ctx.cfg.fileSystems
102 }
103
104 // Returns the active metrics registry for the context
@@ -277,6 +277,14 @@ func (ctx Context) LoadModule(structPointer any, fieldName string) (any, error)
277 return result, nil
278 }
279
280 +// emitEvent is a small convenience method so the caddy core can emit events, if the event app is configured.
281 +func (ctx Context) emitEvent(name string, data map[string]any) Event {
282 + if ctx.cfg == nil || ctx.cfg.eventEmitter == nil {
283 + return Event{}
284 + }
285 + return ctx.cfg.eventEmitter.Emit(ctx, name, data)
286 +}
287 +
288 // loadModulesFromSomeMap loads modules from val, which must be a type of map[string]any.
289 // Depending on inlineModuleKey, it will be interpreted as either a ModuleMap (key is the module
290 // name) or as a regular map (key is not the module name, and module name is defined inline).
@@ -429,6 +437,14 @@ func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (any, error
437
438 ctx.moduleInstances[id] = append(ctx.moduleInstances[id], val)
439
440 + // if the loaded module happens to be an app that can emit events, store it so the
441 + // core can have access to emit events without an import cycle
442 + if ee, ok := val.(eventEmitter); ok {
443 + if _, ok := ee.(App); ok {
444 + ctx.cfg.eventEmitter = ee
445 + }
446 + }
447 +
448 return val, nil
449 }
450
@@ -600,3 +616,11 @@ func (ctx *Context) WithValue(key, value any) Context {
616 exitFuncs: ctx.exitFuncs,
617 }
618 }
619 +
620 +// eventEmitter is a small interface that inverts dependencies for
621 +// the caddyevents package, so the core can emit events without an
622 +// import cycle (i.e. the caddy package doesn't have to import
623 +// the caddyevents package, which imports the caddy package).
624 +type eventEmitter interface {
625 + Emit(ctx Context, eventName string, data map[string]any) Event
626 +}
internal/filesystems/map.go
+16 -16
@@ -7,10 +7,10 @@ import (
7 )
8
9 const (
10 - DefaultFilesystemKey = "default"
10 + DefaultFileSystemKey = "default"
11 )
12
13 -var DefaultFilesystem = &wrapperFs{key: DefaultFilesystemKey, FS: OsFS{}}
13 +var DefaultFileSystem = &wrapperFs{key: DefaultFileSystemKey, FS: OsFS{}}
14
15 // wrapperFs exists so can easily add to wrapperFs down the line
16 type wrapperFs struct {
@@ -18,24 +18,24 @@ type wrapperFs struct {
18 fs.FS
19 }
20
21 -// FilesystemMap stores a map of filesystems
21 +// FileSystemMap stores a map of filesystems
22 // the empty key will be overwritten to be the default key
23 // it includes a default filesystem, based off the os fs
24 -type FilesystemMap struct {
24 +type FileSystemMap struct {
25 m sync.Map
26 }
27
28 // note that the first invocation of key cannot be called in a racy context.
29 -func (f *FilesystemMap) key(k string) string {
29 +func (f *FileSystemMap) key(k string) string {
30 if k == "" {
31 - k = DefaultFilesystemKey
31 + k = DefaultFileSystemKey
32 }
33 return k
34 }
35
36 // Register will add the filesystem with key to later be retrieved
37 // A call with a nil fs will call unregister, ensuring that a call to Default() will never be nil
38 -func (f *FilesystemMap) Register(k string, v fs.FS) {
38 +func (f *FileSystemMap) Register(k string, v fs.FS) {
39 k = f.key(k)
40 if v == nil {
41 f.Unregister(k)
@@ -47,23 +47,23 @@ func (f *FilesystemMap) Register(k string, v fs.FS) {
47 // Unregister will remove the filesystem with key from the filesystem map
48 // if the key is the default key, it will set the default to the osFS instead of deleting it
49 // modules should call this on cleanup to be safe
50 -func (f *FilesystemMap) Unregister(k string) {
50 +func (f *FileSystemMap) Unregister(k string) {
51 k = f.key(k)
52 - if k == DefaultFilesystemKey {
53 - f.m.Store(k, DefaultFilesystem)
52 + if k == DefaultFileSystemKey {
53 + f.m.Store(k, DefaultFileSystem)
54 } else {
55 f.m.Delete(k)
56 }
57 }
58
59 // Get will get a filesystem with a given key
60 -func (f *FilesystemMap) Get(k string) (v fs.FS, ok bool) {
60 +func (f *FileSystemMap) Get(k string) (v fs.FS, ok bool) {
61 k = f.key(k)
62 c, ok := f.m.Load(strings.TrimSpace(k))
63 if !ok {
64 - if k == DefaultFilesystemKey {
65 - f.m.Store(k, DefaultFilesystem)
66 - return DefaultFilesystem, true
64 + if k == DefaultFileSystemKey {
65 + f.m.Store(k, DefaultFileSystem)
66 + return DefaultFileSystem, true
67 }
68 return nil, ok
69 }
@@ -71,7 +71,7 @@ func (f *FilesystemMap) Get(k string) (v fs.FS, ok bool) {
71 }
72
73 // Default will get the default filesystem in the filesystem map
74 -func (f *FilesystemMap) Default() fs.FS {
75 - val, _ := f.Get(DefaultFilesystemKey)
74 +func (f *FileSystemMap) Default() fs.FS {
75 + val, _ := f.Get(DefaultFileSystemKey)
76 return val
77 }
modules/caddyevents/app.go
+20 -90
@@ -20,9 +20,7 @@ import (
20 "errors"
21 "fmt"
22 "strings"
23 - "time"
23
25 - "github.com/google/uuid"
24 "go.uber.org/zap"
25
26 "github.com/caddyserver/caddy/v2"
@@ -206,27 +204,26 @@ func (app *App) On(eventName string, handler Handler) error {
204 //
205 // Note that the data map is not copied, for efficiency. After Emit() is called, the
206 // data passed in should not be changed in other goroutines.
209 -func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) Event {
207 +func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) caddy.Event {
208 logger := app.logger.With(zap.String("name", eventName))
209
212 - id, err := uuid.NewRandom()
210 + e, err := caddy.NewEvent(ctx, eventName, data)
211 if err != nil {
214 - logger.Error("failed generating new event ID", zap.Error(err))
212 + logger.Error("failed to create event", zap.Error(err))
213 }
214
217 - eventName = strings.ToLower(eventName)
218 -
219 - e := Event{
220 - Data: data,
221 - id: id,
222 - ts: time.Now(),
223 - name: eventName,
224 - origin: ctx.Module(),
215 + var originModule caddy.ModuleInfo
216 + var originModuleID caddy.ModuleID
217 + var originModuleName string
218 + if origin := e.Origin(); origin != nil {
219 + originModule = origin.CaddyModule()
220 + originModuleID = originModule.ID
221 + originModuleName = originModule.String()
222 }
223
224 logger = logger.With(
228 - zap.String("id", e.id.String()),
229 - zap.String("origin", e.origin.CaddyModule().String()))
225 + zap.String("id", e.ID().String()),
226 + zap.String("origin", originModuleName))
227
228 // add event info to replacer, make sure it's in the context
229 repl, ok := ctx.Context.Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
@@ -239,15 +236,15 @@ func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) E
236 case "event":
237 return e, true
238 case "event.id":
242 - return e.id, true
239 + return e.ID(), true
240 case "event.name":
244 - return e.name, true
241 + return e.Name(), true
242 case "event.time":
246 - return e.ts, true
243 + return e.Timestamp(), true
244 case "event.time_unix":
248 - return e.ts.UnixMilli(), true
245 + return e.Timestamp().UnixMilli(), true
246 case "event.module":
250 - return e.origin.CaddyModule().ID, true
247 + return originModuleID, true
248 case "event.data":
249 return e.Data, true
250 }
@@ -269,7 +266,7 @@ func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) E
266 // invoke handlers bound to the event by name and also all events; this for loop
267 // iterates twice at most: once for the event name, once for "" (all events)
268 for {
272 - moduleID := e.origin.CaddyModule().ID
269 + moduleID := originModuleID
270
271 // implement propagation up the module tree (i.e. start with "a.b.c" then "a.b" then "a" then "")
272 for {
@@ -292,7 +289,7 @@ func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) E
289 zap.Any("handler", handler))
290
291 if err := handler.Handle(ctx, e); err != nil {
295 - aborted := errors.Is(err, ErrAborted)
292 + aborted := errors.Is(err, caddy.ErrEventAborted)
293
294 logger.Error("handler error",
295 zap.Error(err),
@@ -326,76 +323,9 @@ func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) E
323 return e
324 }
325
329 -// Event represents something that has happened or is happening.
330 -// An Event value is not synchronized, so it should be copied if
331 -// being used in goroutines.
332 -//
333 -// EXPERIMENTAL: As with the rest of this package, events are
334 -// subject to change.
335 -type Event struct {
336 - // If non-nil, the event has been aborted, meaning
337 - // propagation has stopped to other handlers and
338 - // the code should stop what it was doing. Emitters
339 - // may choose to use this as a signal to adjust their
340 - // code path appropriately.
341 - Aborted error
342 -
343 - // The data associated with the event. Usually the
344 - // original emitter will be the only one to set or
345 - // change these values, but the field is exported
346 - // so handlers can have full access if needed.
347 - // However, this map is not synchronized, so
348 - // handlers must not use this map directly in new
349 - // goroutines; instead, copy the map to use it in a
350 - // goroutine.
351 - Data map[string]any
352 -
353 - id uuid.UUID
354 - ts time.Time
355 - name string
356 - origin caddy.Module
357 -}
358 -
359 -func (e Event) ID() uuid.UUID { return e.id }
360 -func (e Event) Timestamp() time.Time { return e.ts }
361 -func (e Event) Name() string { return e.name }
362 -func (e Event) Origin() caddy.Module { return e.origin }
363 -
364 -// CloudEvent exports event e as a structure that, when
365 -// serialized as JSON, is compatible with the
366 -// CloudEvents spec.
367 -func (e Event) CloudEvent() CloudEvent {
368 - dataJSON, _ := json.Marshal(e.Data)
369 - return CloudEvent{
370 - ID: e.id.String(),
371 - Source: e.origin.CaddyModule().String(),
372 - SpecVersion: "1.0",
373 - Type: e.name,
374 - Time: e.ts,
375 - DataContentType: "application/json",
376 - Data: dataJSON,
377 - }
378 -}
379 -
380 -// CloudEvent is a JSON-serializable structure that
381 -// is compatible with the CloudEvents specification.
382 -// See https://cloudevents.io.
383 -type CloudEvent struct {
384 - ID string `json:"id"`
385 - Source string `json:"source"`
386 - SpecVersion string `json:"specversion"`
387 - Type string `json:"type"`
388 - Time time.Time `json:"time"`
389 - DataContentType string `json:"datacontenttype,omitempty"`
390 - Data json.RawMessage `json:"data,omitempty"`
391 -}
392 -
393 -// ErrAborted cancels an event.
394 -var ErrAborted = errors.New("event aborted")
395 -
326 // Handler is a type that can handle events.
327 type Handler interface {
398 - Handle(context.Context, Event) error
328 + Handle(context.Context, caddy.Event) error
329 }
330
331 // Interface guards
modules/caddyfs/filesystem.go
+2 -2
@@ -69,11 +69,11 @@ func (xs *Filesystems) Provision(ctx caddy.Context) error {
69 }
70 // register that module
71 ctx.Logger().Debug("registering fs", zap.String("fs", f.Key))
72 - ctx.Filesystems().Register(f.Key, f.fileSystem)
72 + ctx.FileSystems().Register(f.Key, f.fileSystem)
73 // remember to unregister the module when we are done
74 xs.defers = append(xs.defers, func() {
75 ctx.Logger().Debug("unregistering fs", zap.String("fs", f.Key))
76 - ctx.Filesystems().Unregister(f.Key)
76 + ctx.FileSystems().Unregister(f.Key)
77 })
78 }
79 return nil
modules/caddyhttp/fileserver/matcher.go
+1 -1
@@ -274,7 +274,7 @@ func celFileMatcherMacroExpander() parser.MacroExpander {
274 func (m *MatchFile) Provision(ctx caddy.Context) error {
275 m.logger = ctx.Logger()
276
277 - m.fsmap = ctx.Filesystems()
277 + m.fsmap = ctx.FileSystems()
278
279 if m.Root == "" {
280 m.Root = "{http.vars.root}"
modules/caddyhttp/fileserver/matcher_test.go
+3 -3
@@ -117,7 +117,7 @@ func TestFileMatcher(t *testing.T) {
117 },
118 } {
119 m := &MatchFile{
120 - fsmap: &filesystems.FilesystemMap{},
120 + fsmap: &filesystems.FileSystemMap{},
121 Root: "./testdata",
122 TryFiles: []string{"{http.request.uri.path}", "{http.request.uri.path}/"},
123 }
@@ -229,7 +229,7 @@ func TestPHPFileMatcher(t *testing.T) {
229 },
230 } {
231 m := &MatchFile{
232 - fsmap: &filesystems.FilesystemMap{},
232 + fsmap: &filesystems.FileSystemMap{},
233 Root: "./testdata",
234 TryFiles: []string{"{http.request.uri.path}", "{http.request.uri.path}/index.php"},
235 SplitPath: []string{".php"},
@@ -273,7 +273,7 @@ func TestPHPFileMatcher(t *testing.T) {
273 func TestFirstSplit(t *testing.T) {
274 m := MatchFile{
275 SplitPath: []string{".php"},
276 - fsmap: &filesystems.FilesystemMap{},
276 + fsmap: &filesystems.FileSystemMap{},
277 }
278 actual, remainder := m.firstSplit("index.PHP/somewhere")
279 expected := "index.PHP"
modules/caddyhttp/fileserver/staticfiles.go
+1 -1
@@ -186,7 +186,7 @@ func (FileServer) CaddyModule() caddy.ModuleInfo {
186 func (fsrv *FileServer) Provision(ctx caddy.Context) error {
187 fsrv.logger = ctx.Logger()
188
189 - fsrv.fsmap = ctx.Filesystems()
189 + fsrv.fsmap = ctx.FileSystems()
190
191 if fsrv.FileSystem == "" {
192 fsrv.FileSystem = "{http.vars.fs}"