Compare commits
3 Commits
b87d520b61
...
51897b20bd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51897b20bd | ||
|
|
8701938c75 | ||
|
|
7df8bc1446 |
13
AGENTS.md
13
AGENTS.md
@@ -276,13 +276,16 @@ Checks `X-Forwarded-For` header first (for proxied requests), then falls back to
|
||||
|
||||
### TLS Error Handling
|
||||
|
||||
The server uses a `ServerWrapper` type to intercept and classify errors from `Serve()` and `ServeTLS()`. TLS-specific errors are logged at debug level to reduce log clutter:
|
||||
The server uses a `ServerWrapper` type to intercept and classify errors from `Serve()` and `ServeTLS()`. Two mechanisms:
|
||||
|
||||
- Uses `errors.As` to detect TLS error types: `tls.AlertError`, `tls.CertificateVerificationError`, `tls.ECHRejectionError`, `tls.RecordHeaderError`
|
||||
- Also checks for "tls:" prefix in error messages
|
||||
- Additionally uses `http.Server.ErrorLog` with a custom logger that writes at debug level for net/http's internal TLS-related messages
|
||||
1. **Server errors** (`Serve()` / `ServeTLS()`): Uses `errors.As` to detect TLS error types (`tls.AlertError`, `tls.CertificateVerificationError`, `tls.ECHRejectionError`, `tls.RecordHeaderError`) and checks for "tls:" prefix. TLS-specific errors logged at debug level, others at warn level.
|
||||
|
||||
This ensures client-side TLS errors (handshake failures, certificate issues) don't clutter production logs.
|
||||
2. **net/http internal errors** (`http.Server.ErrorLog`): Uses `DebugLogger()` which routes messages through `slogWriter.Write()`:
|
||||
- TLS-related errors (keywords: "tls", "ssl", "handshake", "certificate") → logged at debug level
|
||||
- Other errors → logged at warn level
|
||||
- Both are filtered by configured `loglevel`. Set `loglevel: debug` to see all messages.
|
||||
|
||||
This ensures client-side TLS errors (handshake failures, certificate issues) don't clutter production logs by default.
|
||||
|
||||
### Compression Level Mapping
|
||||
|
||||
|
||||
@@ -399,7 +399,7 @@ type (
|
||||
*http.Server
|
||||
}
|
||||
|
||||
// responseWriterWrapper wraps http.ResponseWriter to capture status code.
|
||||
// responseWriterWrapper wraps [http.ResponseWriter] to capture status code.
|
||||
responseWriterWrapper struct {
|
||||
http.ResponseWriter
|
||||
statusCode int
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"log/slog"
|
||||
@@ -27,11 +26,9 @@ var (
|
||||
logLevel slog.Level
|
||||
)
|
||||
|
||||
// slogWriter implements [io.Writer] to redirect standard log to slog.
|
||||
type (
|
||||
slogWriter struct {
|
||||
handler slog.Handler
|
||||
}
|
||||
// slogWriter implements [io.Writer] to redirect standard log to slog.
|
||||
slogWriter struct{}
|
||||
)
|
||||
|
||||
// Init sets up logger stuff.
|
||||
@@ -154,9 +151,9 @@ func Debugf(format string, a ...any) {
|
||||
slog.Debug(fmt.Sprintf(format, a...))
|
||||
}
|
||||
|
||||
// DebugLogger returns a standard library logger that writes at the configured log level.
|
||||
// This is useful for assigning to http.Server.ErrorLog to route net/http errors
|
||||
// through our logger while respecting the configured log level.
|
||||
// DebugLogger returns a standard library logger that writes at debug/warn level.
|
||||
// TLS-related errors are logged at debug level, others at warn level.
|
||||
// Both are filtered by the configured log level: set loglevel to debug to see all messages.
|
||||
func DebugLogger() *log.Logger {
|
||||
mu.RLock()
|
||||
defer mu.RUnlock()
|
||||
@@ -167,7 +164,9 @@ func DebugLogger() *log.Logger {
|
||||
|
||||
handler := slog.NewTextHandler(Log, opts)
|
||||
|
||||
return log.New(&slogWriter{handler: handler}, "", 0)
|
||||
_ = handler // satisfy linter (handler configures level for potential future use).
|
||||
|
||||
return log.New(&slogWriter{}, "", 0)
|
||||
}
|
||||
|
||||
// Write implements [io.Writer]. It differentiates TLS/SSL errors from other errors.
|
||||
@@ -183,15 +182,12 @@ func (w *slogWriter) Write(p []byte) (int, error) {
|
||||
strings.Contains(msg, "handshake") ||
|
||||
strings.Contains(msg, "certificate")
|
||||
|
||||
level := slog.LevelWarn
|
||||
if isTLSError {
|
||||
level = slog.LevelDebug
|
||||
Debug(msg)
|
||||
} else {
|
||||
Warn(msg)
|
||||
}
|
||||
|
||||
record := slog.NewRecord(time.Now(), level, msg, 0)
|
||||
|
||||
_ = w.handler.Handle(context.TODO(), record)
|
||||
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
|
||||
@@ -363,7 +363,7 @@ func TestDebugLogger(t *testing.T) {
|
||||
logFilePath := tmpFile.Name()
|
||||
tmpFile.Close()
|
||||
|
||||
if err := Init("info", logFilePath); err != nil {
|
||||
if err := Init("debug", logFilePath); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer Close()
|
||||
@@ -385,4 +385,85 @@ func TestDebugLogger(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestDebugLoggerTLS tests that TLS errors are logged at debug level.
|
||||
func TestDebugLoggerTLS(t *testing.T) {
|
||||
tmpFile, err := os.CreateTemp(t.TempDir(), "test_log_*.log")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
logFilePath := tmpFile.Name()
|
||||
tmpFile.Close()
|
||||
|
||||
if err := Init("debug", logFilePath); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer Close()
|
||||
|
||||
logger := DebugLogger()
|
||||
logger.Print("tls handshake timeout")
|
||||
|
||||
content, err := os.ReadFile(logFilePath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !strings.Contains(string(content), "tls handshake timeout") {
|
||||
t.Error("Expected TLS message in log")
|
||||
}
|
||||
}
|
||||
|
||||
// TestDebugLoggerNonTLS tests that non-TLS errors are logged at warn level.
|
||||
func TestDebugLoggerNonTLS(t *testing.T) {
|
||||
tmpFile, err := os.CreateTemp(t.TempDir(), "test_log_*.log")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
logFilePath := tmpFile.Name()
|
||||
tmpFile.Close()
|
||||
|
||||
if err := Init("warn", logFilePath); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer Close()
|
||||
|
||||
logger := DebugLogger()
|
||||
logger.Print("http: connection reset by peer")
|
||||
|
||||
content, err := os.ReadFile(logFilePath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !strings.Contains(string(content), "http: connection reset by peer") {
|
||||
t.Error("Expected non-TLS message in log")
|
||||
}
|
||||
}
|
||||
|
||||
// TestDebugLoggerLevelFiltering tests that log level filtering works.
|
||||
func TestDebugLoggerLevelFiltering(t *testing.T) {
|
||||
tmpFile, err := os.CreateTemp(t.TempDir(), "test_log_*.log")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
logFilePath := tmpFile.Name()
|
||||
tmpFile.Close()
|
||||
|
||||
if err := Init("info", logFilePath); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer Close()
|
||||
|
||||
logger := DebugLogger()
|
||||
logger.Print("tls handshake timeout") // TLS error at debug level - should be filtered
|
||||
|
||||
content, err := os.ReadFile(logFilePath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if strings.Contains(string(content), "tls handshake timeout") {
|
||||
t.Error("Debug-level TLS message should be filtered at info log level")
|
||||
}
|
||||
}
|
||||
|
||||
/* vim: setlocal ft=go noet ai ts=4 sw=4 sts=4: */
|
||||
|
||||
Reference in New Issue
Block a user