Skip to content

Commit 38cdd62

Browse files
authored
Reduce ACME challenge log noise (#396)
* Log missing ACME challenges at DEBUG The "looking up info for HTTP challenge" logs can become very noisy (they produced ~2M logs in 24 hours for us). Requests can come from external ACME clients and pre-checks for challenges not initiated by the server. So treat this case (where no issuer has stored a matching token) as DEBUG to reduce noise. Note that actionable failures will remain visible: - Storage lookup failures: WARN - Empty or corrupt challenge data: WARN - Certificate issuance/renewal failures: visible via existing error logs * Log canceled challenge lookups at DEBUG A big chunk of the noisy "looking up info for HTTP challenge" logs are "context canceled". So also log these as DEBUG, but only do so when both the lookup error and HTTP request context are canceled.
1 parent cf6f57d commit 38cdd62

3 files changed

Lines changed: 120 additions & 3 deletions

File tree

config.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1161,6 +1161,8 @@ func (cfg *Config) TLSConfig() *tls.Config {
11611161
}
11621162
}
11631163

1164+
var errNoACMEChallengeInfo = errors.New("no active ACME challenge")
1165+
11641166
// getACMEChallengeInfo loads the challenge info from either the internal challenge memory
11651167
// or the external storage (implying distributed solving). The second return value
11661168
// indicates whether challenge info was loaded from external storage. If true, the
@@ -1188,6 +1190,7 @@ func (cfg *Config) getACMEChallengeInfo(ctx context.Context, identifier string,
11881190
var chalInfo acme.Challenge
11891191
var chalInfoBytes []byte
11901192
var tokenKey string
1193+
var challengeFound bool
11911194
for _, issuer := range cfg.Issuers {
11921195
ds := distributedSolver{
11931196
storage: cfg.Storage,
@@ -1197,15 +1200,19 @@ func (cfg *Config) getACMEChallengeInfo(ctx context.Context, identifier string,
11971200
var err error
11981201
chalInfoBytes, err = cfg.Storage.Load(ctx, tokenKey)
11991202
if err == nil {
1203+
challengeFound = true
12001204
break
12011205
}
12021206
if errors.Is(err, fs.ErrNotExist) {
12031207
continue
12041208
}
1205-
return Challenge{}, false, fmt.Errorf("opening distributed challenge token file %s: %v", tokenKey, err)
1209+
return Challenge{}, false, fmt.Errorf("opening distributed challenge token file %s: %w", tokenKey, err)
1210+
}
1211+
if !challengeFound {
1212+
return Challenge{}, false, fmt.Errorf("%w: no information found to solve challenge for identifier: %s", errNoACMEChallengeInfo, identifier)
12061213
}
12071214
if len(chalInfoBytes) == 0 {
1208-
return Challenge{}, false, fmt.Errorf("no information found to solve challenge for identifier: %s", identifier)
1215+
return Challenge{}, false, fmt.Errorf("decoding challenge token file %s: empty data", tokenKey)
12091216
}
12101217

12111218
err := json.Unmarshal(chalInfoBytes, &chalInfo)

httphandlers.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
package certmagic
1616

1717
import (
18+
"context"
19+
"errors"
1820
"fmt"
1921
"net/http"
2022
"net/url"
@@ -114,7 +116,14 @@ func (am *ACMEIssuer) distributedHTTPChallengeSolver(w http.ResponseWriter, r *h
114116
}
115117

116118
// couldn't get challenge info even with distributed solver
117-
am.Logger.Warn("looking up info for HTTP challenge",
119+
log := am.Logger.Warn
120+
switch {
121+
case errors.Is(err, errNoACMEChallengeInfo):
122+
log = am.Logger.Debug
123+
case errors.Is(err, context.Canceled) && errors.Is(r.Context().Err(), context.Canceled):
124+
log = am.Logger.Debug
125+
}
126+
log("looking up info for HTTP challenge",
118127
zap.String("uri", r.RequestURI),
119128
zap.String("identifier", host),
120129
zap.String("remote_addr", r.RemoteAddr),

httphandlers_test.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,28 @@
1515
package certmagic
1616

1717
import (
18+
"context"
19+
"errors"
1820
"net/http"
1921
"net/http/httptest"
2022
"os"
2123
"testing"
24+
25+
"go.uber.org/zap"
26+
"go.uber.org/zap/zapcore"
27+
"go.uber.org/zap/zaptest/observer"
2228
)
2329

30+
type loadResultStorage struct {
31+
Storage
32+
data []byte
33+
err error
34+
}
35+
36+
func (s loadResultStorage) Load(context.Context, string) ([]byte, error) {
37+
return s.data, s.err
38+
}
39+
2440
func TestHTTPChallengeHandlerNoOp(t *testing.T) {
2541
am := &ACMEIssuer{CA: "https://example.com/acme/directory", Logger: defaultTestLogger}
2642
testConfig := &Config{
@@ -59,3 +75,88 @@ func TestHTTPChallengeHandlerNoOp(t *testing.T) {
5975
}
6076
}
6177
}
78+
79+
func TestHTTPChallengeLookupLogLevel(t *testing.T) {
80+
tests := []struct {
81+
name string
82+
storage Storage
83+
cancelRequest bool
84+
wantLevel zapcore.Level
85+
}{
86+
{
87+
name: "no active challenge",
88+
storage: &memoryStorage{},
89+
wantLevel: zap.DebugLevel,
90+
},
91+
{
92+
name: "empty challenge data",
93+
storage: loadResultStorage{
94+
Storage: &memoryStorage{},
95+
data: []byte{},
96+
},
97+
wantLevel: zap.WarnLevel,
98+
},
99+
{
100+
name: "request canceled",
101+
storage: loadResultStorage{
102+
Storage: &memoryStorage{},
103+
err: context.Canceled,
104+
},
105+
cancelRequest: true,
106+
wantLevel: zap.DebugLevel,
107+
},
108+
{
109+
name: "storage operation canceled",
110+
storage: loadResultStorage{
111+
Storage: &memoryStorage{},
112+
err: context.Canceled,
113+
},
114+
wantLevel: zap.WarnLevel,
115+
},
116+
{
117+
name: "storage failure",
118+
storage: loadResultStorage{
119+
Storage: &memoryStorage{},
120+
err: errors.New("storage unavailable"),
121+
},
122+
wantLevel: zap.WarnLevel,
123+
},
124+
}
125+
for _, tt := range tests {
126+
t.Run(tt.name, func(t *testing.T) {
127+
core, logs := observer.New(zap.DebugLevel)
128+
logger := zap.New(core)
129+
am := &ACMEIssuer{
130+
CA: "https://example.com/acme/directory",
131+
Logger: logger,
132+
}
133+
am.config = &Config{
134+
Issuers: []Issuer{am},
135+
Storage: tt.storage,
136+
Logger: logger,
137+
}
138+
139+
req := httptest.NewRequest(
140+
http.MethodGet,
141+
"http://example.com/.well-known/acme-challenge/token",
142+
nil,
143+
)
144+
if tt.cancelRequest {
145+
ctx, cancel := context.WithCancel(req.Context())
146+
cancel()
147+
req = req.WithContext(ctx)
148+
}
149+
if am.HandleHTTPChallenge(httptest.NewRecorder(), req) {
150+
t.Fatal("expected challenge request not to be handled")
151+
}
152+
153+
entries := logs.FilterMessage("looking up info for HTTP challenge").All()
154+
if len(entries) != 1 {
155+
t.Fatalf("expected one challenge lookup log, got %d", len(entries))
156+
}
157+
if entries[0].Level != tt.wantLevel {
158+
t.Fatalf("expected log level %s, got %s", tt.wantLevel, entries[0].Level)
159+
}
160+
})
161+
}
162+
}

0 commit comments

Comments
 (0)