Skip to content

Commit ef56ed4

Browse files
committed
fix: add ability to disable logs
1 parent 429abb0 commit ef56ed4

16 files changed

Lines changed: 401 additions & 38 deletions

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,8 @@ wait0 caches only `GET` responses with a `2xx` status, an allowed `Content-Type`
147147

148148
An origin response that fails these checks is served as `X-Wait0: bypass` without being stored, with `X-Wait0-Reason` explaining why. If background revalidation receives a disallowed content type, either cache-control directive, or a non-`2xx` status, the existing entry is deleted; a network error leaves it available. A stale response is reported as `X-Wait0: hit`.
149149

150+
`logging.debug_headers` controls wait0 diagnostic response headers and the markers sent to the origin during revalidation. Omit it to enable every supported header, use a subset to select individual headers, or set `debug_headers: []` to disable all diagnostics. Functional headers such as `X-Wait0-CSRF` and origin-provided `X-Wait0-Tag` are not controlled by this option.
151+
150152
### Query parameter caching
151153

152154
Cache keys use only the URL path by default, so `/blog`, `/blog?page=1`, and `/blog?utm_source=email` share the same cached response. The first cold request reaches the origin with its complete query and fills the shared entry; later background refreshes omit query parameters that are not in `varyByQueryParams`.
@@ -249,6 +251,18 @@ rules:
249251
maxRequestsAtATime: 20
250252
251253
logging:
254+
# Diagnostic response headers and origin revalidation markers. Omit this
255+
# option to enable all supported headers by default.
256+
debug_headers:
257+
- X-Wait0
258+
- X-Wait0-Reason
259+
- X-Wait0-Revalidated-At
260+
- X-Wait0-Revalidated-By
261+
- X-Wait0-Discovered-By
262+
- X-Wait0-Revalidate-At
263+
- X-Wait0-Revalidate-Entropy
264+
# To disable every diagnostic header, replace the list above with:
265+
# debug_headers: []
252266
# Logs a stats snapshot at this interval; omit to disable.
253267
log_stats_every: '1m'
254268
# Logs a summary after each warmup batch.

debug/wait0.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,18 @@ rules:
4949
maxRequestsAtATime: 1000
5050

5151
logging:
52+
# Diagnostic response headers and origin revalidation markers.
53+
# Omit debug_headers to enable all supported headers by default.
54+
debug_headers:
55+
- X-Wait0
56+
- X-Wait0-Reason
57+
- X-Wait0-Revalidated-At
58+
- X-Wait0-Revalidated-By
59+
- X-Wait0-Discovered-By
60+
- X-Wait0-Revalidate-At
61+
- X-Wait0-Revalidate-Entropy
62+
# To disable every diagnostic header, replace the list above with:
63+
# debug_headers: []
5264
# log stats when enabled logs
5365
log_stats_every: '10s'
5466
log_warmup: true

docs/api-endpoints.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,17 +50,19 @@ An origin response is cacheable only when:
5050

5151
`cachableContentType` defaults to `text/html` and `application/xhtml+xml`. Matching is case-insensitive and ignores media-type parameters such as `charset=utf-8`. A missing or malformed `Content-Type` is not cacheable.
5252

53-
## Response headers added by wait0
53+
## Diagnostic headers added by wait0
5454

5555
| Header | When present | Meaning |
5656
|--------|--------------|---------|
57-
| `X-Wait0` | always on handled responses | Cache/proxy decision marker |
57+
| `X-Wait0` | handled responses when enabled | Cache/proxy decision marker |
5858
| `X-Wait0-Reason` | response bypassed or failed | Machine-readable reason the response was not cached |
5959
| `X-Wait0-Revalidated-At` | cache `hit` with revalidation metadata | Last revalidation timestamp (RFC3339Nano) |
6060
| `X-Wait0-Revalidated-By` | with `X-Wait0-Revalidated-At` | Revalidation source (`user`, `warmup`, `invalidate`, etc.) |
6161
| `X-Wait0-Discovered-By` | if entry was discovery seeded | Discovery source marker |
6262
| `Access-Control-Expose-Headers` | when wait0 headers exist | Exposes wait0 headers to browser clients |
6363

64+
During background revalidation, wait0 can also send `X-Wait0-Revalidate-At` and `X-Wait0-Revalidate-Entropy` to the origin. `logging.debug_headers` controls all seven diagnostic headers. Omission enables all; an explicit `debug_headers: []` disables all. A configured subset enables only the named headers. `WAIT0_SEND_REVALIDATE_MARKERS=false` remains a master disable for the two origin markers.
65+
6466
## Example
6567

6668
```bash

docs/for-developers.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ For dashboard:
157157

158158
| Field | Type | Notes |
159159
|-------|------|------|
160+
| `debug_headers` | string array | Diagnostic headers to emit; omitted enables all seven supported headers, while an explicit `[]` disables all |
160161
| `log_stats_every` | duration | Enables periodic stats logging (`> 0`) |
161162
| `log_warmup` | bool | Emits warmup batch summaries |
162163
| `log_url_autodiscover` | bool | Emits per-sitemap discovery logs |
@@ -174,6 +175,8 @@ For dashboard:
174175
- Keep static assets in CDN, Nginx, and browser caches; wait0 defaults to caching only dynamic HTML/XHTML SWR responses.
175176
- Origin `2xx` responses with `Cache-Control: no-cache` or `no-store` are not stored; either directive received during revalidation deletes the existing entry.
176177
- Warmup loops never overlap: wait0 completes the current rule snapshot, waits `pauseBetweenRuns`, then loads a fresh snapshot.
178+
- `logging.debug_headers` may select any of `X-Wait0`, `X-Wait0-Reason`, `X-Wait0-Revalidated-At`, `X-Wait0-Revalidated-By`, `X-Wait0-Discovered-By`, `X-Wait0-Revalidate-At`, and `X-Wait0-Revalidate-Entropy`.
179+
- Omit `logging.debug_headers` to enable all diagnostics; set `debug_headers: []` to disable all of them.
177180
- `X-Wait0` response header identifies behavior (`hit`, `miss`, `bypass`, `ignore-by-cookie`, `ignore-by-status`, `bad-gateway`).
178181
- `X-Wait0-Reason` identifies why a response was bypassed or failed (for example, `bypass-rule` or `non-cacheable-content-type`).
179182

internal/wait0/config.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ type Config struct {
4848
Logging struct {
4949
LogStatsEvery string `yaml:"log_stats_every"`
5050
logStatsEveryDur time.Duration `yaml:"-"`
51+
// DebugHeaders controls wait0 diagnostic response headers and origin
52+
// revalidation markers. Omitted enables all; an explicit [] disables all.
53+
DebugHeaders []string `yaml:"debug_headers"`
5154
// LogWarmUp prints a summary after each warmup batch drains.
5255
LogWarmUp bool `yaml:"log_warmup"`
5356

@@ -189,6 +192,12 @@ func LoadConfig(path string) (Config, error) {
189192
cfg.Logging.logStatsEveryDur = d
190193
}
191194

195+
debugHeaders, err := proxy.NormalizeDebugHeaders(cfg.Logging.DebugHeaders)
196+
if err != nil {
197+
return Config{}, fmt.Errorf("logging.debug_headers: %w", err)
198+
}
199+
cfg.Logging.DebugHeaders = debugHeaders
200+
192201
if strings.TrimSpace(cfg.Logging.LogRevalidationEvery) != "" {
193202
// Backward compatible alias for the previous warmup logging setting.
194203
_, err := time.ParseDuration(cfg.Logging.LogRevalidationEvery)

internal/wait0/config_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"strings"
99
"testing"
1010
"time"
11+
12+
"wait0/internal/wait0/proxy"
1113
)
1214

1315
func TestLoadConfig_ValidAndCompiledFields(t *testing.T) {
@@ -56,6 +58,9 @@ rules:
5658
if cfg.Logging.logStatsEveryDur != 10*time.Second {
5759
t.Fatalf("logStatsEveryDur = %s", cfg.Logging.logStatsEveryDur)
5860
}
61+
if got, want := cfg.Logging.DebugHeaders, proxy.DefaultDebugHeaders(); !slicesEqual(got, want) {
62+
t.Fatalf("default debug_headers = %v, want %v", got, want)
63+
}
5964
if cfg.URLsDiscover.initialDelayDur != 2*time.Second {
6065
t.Fatalf("initialDelayDur = %s", cfg.URLsDiscover.initialDelayDur)
6166
}
@@ -147,6 +152,7 @@ func TestLoadConfig_Errors(t *testing.T) {
147152
{name: "bad warmup", yaml: "storage:\n ram: {max: \"1m\"}\n disk: {max: \"1m\"}\nserver:\n origin: \"http://x\"\nrules:\n - match: \"PathPrefix(/)\"\n warmUp:\n pauseBetweenRuns: \"\"\n maxRequestsAtATime: 1\n"},
148153
{name: "both warmup intervals", yaml: "storage:\n ram: {max: \"1m\"}\n disk: {max: \"1m\"}\nserver:\n origin: \"http://x\"\nrules:\n - match: \"PathPrefix(/)\"\n warmUp:\n pauseBetweenRuns: \"10s\"\n runEvery: \"10s\"\n maxRequestsAtATime: 1\n"},
149154
{name: "bad log stats", yaml: "storage:\n ram: {max: \"1m\"}\n disk: {max: \"1m\"}\nserver:\n origin: \"http://x\"\nlogging:\n log_stats_every: \"bad\"\nrules: []\n"},
155+
{name: "bad debug header", yaml: "storage:\n ram: {max: \"1m\"}\n disk: {max: \"1m\"}\nserver:\n origin: \"http://x\"\nlogging:\n debug_headers: [\"X-Wait0-Unknown\"]\nrules: []\n"},
150156
{name: "duplicate auth token ids", yaml: "storage:\n ram: {max: \"1m\"}\n disk: {max: \"1m\"}\nserver:\n origin: \"http://x\"\n invalidation:\n enabled: true\nauth:\n tokens:\n - id: \"dup\"\n token: \"a\"\n scopes: [\"invalidation:write\"]\n - id: \"dup\"\n token: \"b\"\n scopes: [\"invalidation:write\"]\nrules: []\n"},
151157
{name: "invalidation enabled without auth scope", yaml: "storage:\n ram: {max: \"1m\"}\n disk: {max: \"1m\"}\nserver:\n origin: \"http://x\"\n invalidation:\n enabled: true\nauth:\n tokens:\n - id: \"x\"\n token: \"t\"\n scopes: [\"other:scope\"]\nrules: []\n"},
152158
}
@@ -164,6 +170,30 @@ func TestLoadConfig_Errors(t *testing.T) {
164170
}
165171
}
166172

173+
func TestLoadConfig_ExplicitEmptyDebugHeadersDisablesAll(t *testing.T) {
174+
cfgPath := filepath.Join(t.TempDir(), "wait0.yaml")
175+
yaml := `storage:
176+
ram: {max: "1m"}
177+
disk: {max: "1m"}
178+
server:
179+
origin: "http://x"
180+
logging:
181+
debug_headers: []
182+
rules: []
183+
`
184+
if err := os.WriteFile(cfgPath, []byte(yaml), 0o644); err != nil {
185+
t.Fatalf("WriteFile: %v", err)
186+
}
187+
188+
cfg, err := LoadConfig(cfgPath)
189+
if err != nil {
190+
t.Fatalf("LoadConfig: %v", err)
191+
}
192+
if cfg.Logging.DebugHeaders == nil || len(cfg.Logging.DebugHeaders) != 0 {
193+
t.Fatalf("debug_headers = %#v, want non-nil empty list", cfg.Logging.DebugHeaders)
194+
}
195+
}
196+
167197
func TestLoadConfig_AuthTokenEnvOverride(t *testing.T) {
168198
t.Setenv("WAIT0_INV_TOKEN", "from-env-token")
169199

@@ -205,6 +235,18 @@ rules: []
205235
}
206236
}
207237

238+
func slicesEqual(a, b []string) bool {
239+
if len(a) != len(b) {
240+
return false
241+
}
242+
for i := range a {
243+
if a[i] != b[i] {
244+
return false
245+
}
246+
}
247+
return true
248+
}
249+
208250
func TestLoadConfig_DebugConfigHasFixedLocalAuthToken(t *testing.T) {
209251
t.Setenv("WAIT0_API_AUTH_TOKEN", "must-not-override-debug-config")
210252

internal/wait0/handler_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,33 @@ func TestHandle_CacheMissThenHit(t *testing.T) {
4545
}
4646
}
4747

48+
func TestHandle_ExplicitEmptyDebugHeadersEmitsNone(t *testing.T) {
49+
origin := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
50+
w.Header().Set("Content-Type", "text/html")
51+
for _, name := range proxy.DefaultDebugHeaders() {
52+
w.Header().Set(name, "origin-spoof")
53+
}
54+
fmt.Fprint(w, "ok")
55+
}))
56+
defer origin.Close()
57+
58+
rule := mustRule(t, "PathPrefix(/)")
59+
s := newTestService(t, origin.URL, []Rule{rule})
60+
s.debugHeaders = proxy.NewDebugHeaderSet([]string{})
61+
62+
w := httptest.NewRecorder()
63+
s.Handler().ServeHTTP(w, httptest.NewRequest(http.MethodGet, "http://wait0.local/page", nil))
64+
65+
if got := w.Result().StatusCode; got != http.StatusOK {
66+
t.Fatalf("status = %d, want 200", got)
67+
}
68+
for _, name := range proxy.DefaultDebugHeaders() {
69+
if got := w.Result().Header.Get(name); got != "" {
70+
t.Fatalf("disabled debug header %s = %q", name, got)
71+
}
72+
}
73+
}
74+
4875
func TestHandle_NonCachableContentTypeByDefault(t *testing.T) {
4976
var hits atomic.Int32
5077
origin := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

internal/wait0/proxy/controller.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ type Runtime interface {
1515
FetchFromOrigin(r *http.Request) (Entry, bool, string, error)
1616
Store(key string, ent Entry)
1717
RevalidateAsync(key, path, query string)
18+
DebugHeaders() DebugHeaderSet
1819
WriteEntryWithStats(w http.ResponseWriter, ent Entry, wait0, reason string)
1920
}
2021

@@ -80,7 +81,7 @@ func (c *Controller) Handle(w http.ResponseWriter, r *http.Request) {
8081

8182
respEnt, cacheable, statusKind, err := c.rt.FetchFromOrigin(r)
8283
if err != nil {
83-
SetWait0Headers(w.Header(), "bad-gateway", "origin-error")
84+
SetWait0Headers(w.Header(), "bad-gateway", "origin-error", c.rt.DebugHeaders())
8485
http.Error(w, "bad gateway", http.StatusBadGateway)
8586
return
8687
}
@@ -110,7 +111,7 @@ func (c *Controller) Handle(w http.ResponseWriter, r *http.Request) {
110111
func (c *Controller) proxyPass(w http.ResponseWriter, r *http.Request, wait0, reason string) {
111112
ent, _, _, err := c.rt.FetchFromOrigin(r)
112113
if err != nil {
113-
SetWait0Headers(w.Header(), "bad-gateway", "origin-error")
114+
SetWait0Headers(w.Header(), "bad-gateway", "origin-error", c.rt.DebugHeaders())
114115
http.Error(w, "bad gateway", http.StatusBadGateway)
115116
return
116117
}

internal/wait0/proxy/controller_test.go

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ type fakeRuntime struct {
3030
revalidated []struct{ key, path, query string }
3131
writeWait0 []string
3232
writeReasons []string
33+
debugHeaders DebugHeaderSet
3334
}
3435

3536
func (f *fakeRuntime) HandleControl(http.ResponseWriter, *http.Request) bool {
@@ -56,13 +57,15 @@ func (f *fakeRuntime) RevalidateAsync(key, path, query string) {
5657
f.revalidated = append(f.revalidated, struct{ key, path, query string }{key: key, path: path, query: query})
5758
}
5859

60+
func (f *fakeRuntime) DebugHeaders() DebugHeaderSet { return f.debugHeaders }
61+
5962
func (f *fakeRuntime) WriteEntryWithStats(w http.ResponseWriter, ent Entry, wait0, reason string) {
6063
f.writeWait0 = append(f.writeWait0, wait0)
6164
f.writeReasons = append(f.writeReasons, reason)
6265
if ent.Status == 0 {
6366
ent.Status = http.StatusOK
6467
}
65-
WriteEntry(w, ent, wait0, reason)
68+
WriteEntry(w, ent, wait0, reason, f.debugHeaders)
6669
}
6770

6871
func TestController_Handle_ShortCircuitsControl(t *testing.T) {
@@ -323,3 +326,24 @@ func TestController_Handle_OriginBranches(t *testing.T) {
323326
})
324327
}
325328
}
329+
330+
func TestController_Handle_OriginErrorWithDebugHeadersDisabled(t *testing.T) {
331+
rt := &fakeRuntime{
332+
originErr: errors.New("boom"),
333+
debugHeaders: NewDebugHeaderSet([]string{}),
334+
}
335+
c := NewController(rt)
336+
w := httptest.NewRecorder()
337+
r := httptest.NewRequest(http.MethodGet, "http://wait0.local/origin", nil)
338+
339+
c.Handle(w, r)
340+
341+
if got := w.Result().StatusCode; got != http.StatusBadGateway {
342+
t.Fatalf("status = %d, want %d", got, http.StatusBadGateway)
343+
}
344+
for _, name := range DefaultDebugHeaders() {
345+
if got := w.Result().Header.Get(name); got != "" {
346+
t.Fatalf("disabled header %s = %q", name, got)
347+
}
348+
}
349+
}

0 commit comments

Comments
 (0)