@marcel91200 / caddy Mirror · read-only / commits / 4978c54f
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 11, 04:45 UTC

events: tell CertMagic which events are worth emitting (#8010)

CertMagic assembles an event's data before emit() can look at OnEvent, because Go evaluates arguments first, so tls_get_certificate costs a map and a ClientHello copy on every handshake even when nothing will read them. caddyserver/certmagic#406 added Config.ShouldEmitFunc so an embedder can answer that question first. Export App.ShouldEmit and pass it along with OnEvent. The predicate has to account for every observer, not just subscribed handlers: Emit writes a debug line for each event, so an event is observable whenever debug logging is on. Emit now asks the same method it exports, so the two cannot drift. Benchmarks are included; measurements are in the pull request.

Y.Horie committed Oct 3, 2026 at 00:03 UTC 4978c54f81aabbdaab11dedfa335cf89bf538bc2
5 files changed +75 -16
modules/caddyevents/app.go
+16 -8
@@ -199,6 +199,17 @@ func (app *App) On(eventName string, handler Handler) error {
199 })
200 }
201
202 +// ShouldEmit reports whether emitting the named event could be observed by
203 +// anything: a handler subscribed to it by name or to all events, or the debug
204 +// log that Emit writes for every event. Emitters that assemble event data
205 +// before calling Emit can use it to skip that work; it is the same question
206 +// Emit answers internally, exported so callers do not have to guess.
207 +func (app *App) ShouldEmit(eventName string) bool {
208 + return app.subscriptions[eventName] != nil ||
209 + app.subscriptions[""] != nil ||
210 + app.logger.Core().Enabled(zapcore.DebugLevel)
211 +}
212 +
213 // Emit creates and dispatches an event named eventName to all relevant handlers with
214 // the metadata data. Events are emitted and propagated synchronously. The returned Event
215 // value will have any additional information from the invoked handlers.
@@ -212,14 +223,11 @@ func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) c
223 zap.String("name", eventName), zap.Error(err))
224 }
225
215 - // A handler can only be reached through subscriptions to this event by
216 - // name or to all events, so if neither is bound, nothing can observe
217 - // this event and the only remaining output is the debug log below.
218 - // Bail out before deriving loggers and registering replacer values:
219 - // some events, such as tls_get_certificate, are emitted on every TLS
220 - // handshake, where that work is significant and always wasted.
221 - if app.subscriptions[eventName] == nil && app.subscriptions[""] == nil &&
222 - !app.logger.Core().Enabled(zapcore.DebugLevel) {
226 + // bail out before deriving loggers and registering replacer values if
227 + // nothing can observe this event: some events, such as
228 + // tls_get_certificate, are emitted on every TLS handshake, where that
229 + // work is significant and always wasted
230 + if !app.ShouldEmit(eventName) {
231 return e
232 }
233
modules/caddyevents/app_test.go
+48
@@ -92,6 +92,54 @@ func TestEmitDispatchesToCatchAllSubscriber(t *testing.T) {
92 }
93 }
94
95 +// Emit writes a debug line for every event, so with debug logging on an event
96 +// is observable even when nothing is subscribed. A predicate that answered
97 +// only about subscribers would silence that line.
98 +func TestShouldEmit(t *testing.T) {
99 + app, _, cancel := testApp(t)
100 + defer cancel()
101 +
102 + if app.ShouldEmit("cert_obtained") {
103 + t.Error("nothing is subscribed and debug is off; should not emit")
104 + }
105 +
106 + if err := app.On("cert_obtained", new(countingHandler)); err != nil {
107 + t.Fatal(err)
108 + }
109 + if !app.ShouldEmit("cert_obtained") {
110 + t.Error("a handler is bound by name; should emit")
111 + }
112 + if app.ShouldEmit("tls_get_certificate") {
113 + t.Error("nothing is bound to this event; should not emit")
114 + }
115 +}
116 +
117 +func TestShouldEmitWithCatchAllSubscriber(t *testing.T) {
118 + app, _, cancel := testApp(t)
119 + defer cancel()
120 +
121 + if err := app.Subscribe(&Subscription{Handlers: []Handler{new(countingHandler)}}); err != nil {
122 + t.Fatal(err)
123 + }
124 + if !app.ShouldEmit("tls_get_certificate") {
125 + t.Error("a catch-all subscription observes every event; should emit")
126 + }
127 +}
128 +
129 +func TestShouldEmitWithDebugLogging(t *testing.T) {
130 + app, _, cancel := testApp(t)
131 + defer cancel()
132 + app.logger = zap.New(zapcore.NewCore(
133 + zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()),
134 + zapcore.AddSync(io.Discard),
135 + zapcore.DebugLevel,
136 + ))
137 +
138 + if !app.ShouldEmit("tls_get_certificate") {
139 + t.Error("debug logging observes every event; should emit")
140 + }
141 +}
142 +
143 // Some events, such as tls_get_certificate, are emitted on every TLS
144 // handshake, whether or not anything is subscribed to them.
145 func BenchmarkEmitNoSubscribers(b *testing.B) {
modules/caddyevents/handshake_bench_test.go
+6 -5
@@ -57,7 +57,7 @@ func benchCert(tb testing.TB) tls.Certificate {
57 // benchmarkCertLookup measures CertMagic's GetCertificate, which Caddy calls
58 // once per TLS handshake. CertMagic emits "tls_get_certificate" there, so this
59 // shows what the events app costs a handshake when nothing is subscribed.
60 -func benchmarkCertLookup(b *testing.B, onEvent func(context.Context, string, map[string]any) error) {
60 +func benchmarkCertLookup(b *testing.B, onEvent func(context.Context, string, map[string]any) error, shouldEmit func(string) bool) {
61 b.Helper()
62
63 var cfg *certmagic.Config
@@ -68,9 +68,10 @@ func benchmarkCertLookup(b *testing.B, onEvent func(context.Context, string, map
68 b.Cleanup(cache.Stop)
69
70 cfg = certmagic.New(cache, certmagic.Config{
71 - Storage: &certmagic.FileStorage{Path: b.TempDir()},
72 - Logger: zap.NewNop(),
73 - OnEvent: onEvent,
71 + Storage: &certmagic.FileStorage{Path: b.TempDir()},
72 + Logger: zap.NewNop(),
73 + OnEvent: onEvent,
74 + ShouldEmitFunc: shouldEmit,
75 })
76 if _, err := cfg.CacheUnmanagedTLSCertificate(context.Background(), benchCert(b), nil); err != nil {
77 b.Fatal(err)
@@ -107,5 +108,5 @@ func BenchmarkCertLookupWithEventsApp(b *testing.B) {
108
109 benchmarkCertLookup(b, func(_ context.Context, name string, data map[string]any) error {
110 return app.Emit(ctx, name, data).Aborted
110 - })
111 + }, app.ShouldEmit)
112 }
modules/caddytls/automation.go
+1
@@ -366,6 +366,7 @@ func (ap *AutomationPolicy) makeCertMagicConfig(tlsApp *TLS, issuers []certmagic
366 RenewalWindowRatio: ap.RenewalWindowRatio,
367 KeySource: keySource,
368 OnEvent: tlsApp.onEvent,
369 + ShouldEmitFunc: tlsApp.events.ShouldEmit,
370 OnDemand: ond,
371 ReusePrivateKeys: ap.ReusePrivateKeys,
372 OCSP: certmagic.OCSPConfig{
modules/caddytls/tls.go
+4 -3
@@ -255,9 +255,10 @@ func (t *TLS) Provision(ctx caddy.Context) error {
255 // commands like validate can be a better test
256 certCacheMu.RLock()
257 magic := certmagic.New(certCache, certmagic.Config{
258 - Storage: ctx.Storage(),
259 - Logger: t.logger,
260 - OnEvent: t.onEvent,
258 + Storage: ctx.Storage(),
259 + Logger: t.logger,
260 + OnEvent: t.onEvent,
261 + ShouldEmitFunc: t.events.ShouldEmit,
262 OCSP: certmagic.OCSPConfig{
263 DisableStapling: t.DisableOCSPStapling,
264 },