Move ssl/tls related error to debug loglevel, to avoid log cluttering.

This commit is contained in:
eleksir
2026-04-04 13:16:17 +03:00
parent 14b7a17b00
commit 958ef67f29
5 changed files with 162 additions and 14 deletions

View File

@@ -257,6 +257,16 @@ Checks `X-Forwarded-For` header first (for proxied requests), then falls back to
- `WriteTimeout`: configurable via `backup_timeout` (converted to `time.Duration`)
- TLS minimum version: TLS 1.3 (when HTTPS enabled)
### 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:
- 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
This ensures client-side TLS errors (handshake failures, certificate issues) don't clutter production logs.
## Dependencies
- `github.com/hjson/hjson-go` v3.3.0 — HJSON config parsing

View File

@@ -111,6 +111,19 @@ good enough cypher.
And one more thing user and password are mandatory, we should have at least some minimal barriers, right?
## Logging
Backer logs events via Go's `slog`. Log levels: error, warn, info, debug.
### TLS Error Handling
TLS handshake errors and other client-side SSL/TLS errors are logged at **debug** level to reduce log clutter.
This includes:
- Certificate verification failures
- Handshake failures
- Protocol errors
- TLS-specific connection issues
## Special Thanks
This project was developed with the assistance of **opencode** (AI co-programmer powered by MIMO). The AI helped with:

View File

@@ -2,6 +2,7 @@
package main
import (
"errors"
"flag"
"fmt"
"net/http"
@@ -43,7 +44,7 @@ func main() {
log.Infof("Starting backer version %s at %s", version, server.Addr)
if backer.C.NoHTTPS {
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
if err := server.Serve(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("Failed to start server: %v", err)
}
} else {
@@ -55,7 +56,7 @@ func main() {
log.Fatalf("Certificate key file error: %v", err)
}
if err := server.ListenAndServeTLS(backer.C.Cert, backer.C.Key); err != nil && err != http.ErrServerClosed {
if err := server.ServeTLS(backer.C.Cert, backer.C.Key); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("Failed to start server: %v", err)
}
}

View File

@@ -3,6 +3,7 @@ package backer
import (
"context"
"crypto/tls"
"errors"
"fmt"
"io"
"net"
@@ -14,6 +15,36 @@ import (
"backer/internal/log"
)
// isTLSError checks if the given error is TLS-related.
// It checks for both TLS-specific error types and error messages containing "tls:".
func isTLSError(err error) bool {
if err == nil {
return false
}
var alertErr *tls.AlertError
if errors.As(err, &alertErr) && alertErr != nil {
return true
}
var certErr *tls.CertificateVerificationError
if errors.As(err, &certErr) && certErr != nil {
return true
}
var echErr *tls.ECHRejectionError
if errors.As(err, &echErr) && echErr != nil {
return true
}
var recordErr *tls.RecordHeaderError
if errors.As(err, &recordErr) && recordErr != nil {
return true
}
return strings.HasPrefix(err.Error(), "tls:")
}
// withServerHeader wraps a handler to add Server header to all responses.
func withServerHeader(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -23,7 +54,8 @@ func withServerHeader(next http.Handler) http.Handler {
}
// NewServer starts new server instance with given config file.
func NewServer(configPath string) (*http.Server, error) {
// It returns a serverWrapper that intercepts and classifies server errors.
func NewServer(configPath string) (*serverWrapper, error) { //nolint: revive
_ = log.Init("info", "") // Initialize logger with defaults for early logging.
if err := LoadConfig(configPath); err != nil {
@@ -139,15 +171,19 @@ func NewServer(configPath string) (*http.Server, error) {
// HTTP mode if nohttps is enabled in config.
// HTTPS mode if nohttps is disabled in config.
if C.NoHTTPS {
return &http.Server{
return &serverWrapper{
Server: &http.Server{
Addr: fmt.Sprintf("%s:%d", C.Address, C.Port),
Handler: withServerHeader(mux),
ReadHeaderTimeout: readHeaderTimeout,
WriteTimeout: time.Duration(C.BackupTimeout) * time.Minute,
ErrorLog: log.DebugLogger(),
},
}, nil
}
return &http.Server{
return &serverWrapper{
Server: &http.Server{
Addr: fmt.Sprintf("%s:%d", C.Address, C.Port),
Handler: withServerHeader(mux),
ReadHeaderTimeout: readHeaderTimeout,
@@ -155,6 +191,8 @@ func NewServer(configPath string) (*http.Server, error) {
TLSConfig: &tls.Config{
MinVersion: tls.VersionTLS13,
},
ErrorLog: log.DebugLogger(),
},
}, nil
}
@@ -235,4 +273,42 @@ func getCompressionAlgorithm(requestedExt string) string {
}
}
// serverWrapper wraps http.Server to intercept and classify errors.
type (
serverWrapper struct {
*http.Server
}
)
// Serve starts the server and classifies any errors returned.
// TLS-related errors are logged at debug level, other errors at warn level.
func (sw *serverWrapper) Serve() error {
err := sw.ListenAndServe()
if err != nil {
if isTLSError(err) {
log.Debugf("Server error: %v", err)
} else {
log.Warnf("Server error: %v", err)
}
}
return err
}
// ServeTLS starts the TLS server and classifies any errors returned.
func (sw *serverWrapper) ServeTLS(certFile, keyFile string) error {
err := sw.ListenAndServeTLS(certFile, keyFile)
if err != nil {
if isTLSError(err) {
log.Debugf("Server TLS error: %v", err)
} else {
log.Warnf("Server TLS error: %v", err)
}
}
return err
}
/* vim: setlocal ft=go noet ai ts=4 sw=4 sts=4: */

View File

@@ -2,9 +2,12 @@
package log
import (
"context"
"fmt"
"log"
"log/slog"
"os"
"strings"
"time"
)
@@ -13,6 +16,13 @@ var (
Log *os.File
)
// slogWriter implements io.Writer to redirect standard log to slog.
type (
slogWriter struct {
handler slog.Handler
}
)
// Init sets up logger stuff.
// level can be error, warn, info, debug; if something other is supplied, info level is selected.
func Init(level, filename string) error {
@@ -122,6 +132,44 @@ func Debugf(format string, a ...any) {
slog.Debug(fmt.Sprintf(format, a...))
}
// DebugLogger returns a standard library logger that writes to debug level.
// This is useful for assigning to http.Server.ErrorLog to move TLS handshake
// and other client-side errors to debug level.
func DebugLogger() *log.Logger {
opts := &slog.HandlerOptions{
Level: slog.LevelDebug,
}
handler := slog.NewTextHandler(Log, opts)
return log.New(&slogWriter{handler: handler}, "", 0)
}
// Write implements io.Writer. It differentiates TLS/SSL errors from other errors.
// TLS-related errors are logged at debug level, while other errors (handler issues,
// unexpected behavior) are logged at warn level.
func (w *slogWriter) Write(p []byte) (int, error) {
msg := string(p)
isTLSError := strings.Contains(msg, "tls") ||
strings.Contains(msg, "ssl") ||
strings.Contains(msg, "TLS") ||
strings.Contains(msg, "SSL") ||
strings.Contains(msg, "handshake") ||
strings.Contains(msg, "certificate")
level := slog.LevelWarn
if isTLSError {
level = slog.LevelDebug
}
record := slog.NewRecord(time.Now(), level, msg, 0)
_ = w.handler.Handle(context.TODO(), record)
return len(p), nil
}
// Fatal logs args at error level and quits with status code 1.
func Fatal(args ...any) {
slog.Error(fmt.Sprint(args...))