Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Give users control over what and how logging happens in the middlewares #472

Merged
merged 21 commits into from
Aug 17, 2024
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Most recent version is listed first.
# v0.1.7
- Update go version; https://github.com/komuw/ong/pull/469
- ong/cry: Replace scrypt with argon2id: https://github.com/komuw/ong/pull/471
- ong/middleware: Give users control over what and how logging happens in the middlewares: https://github.com/komuw/ong/pull/472

# v0.1.6
- Bump versions of dependencies used
Expand Down
74 changes: 31 additions & 43 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package config

import (
"crypto/x509"
"errors"
"fmt"
"log/slog"
"net/http"
Expand All @@ -16,12 +17,6 @@ import (
"github.com/komuw/ong/internal/key"
)

// logging middleware.
const (
// DefaultRateShedSamplePercent is the percentage of rate limited or loadshed responses that will be logged as errors, by default.
DefaultRateShedSamplePercent = 10
)

// ratelimit middleware.
const (
// DefaultRateLimit is the maximum requests allowed (from one IP address) per second, by default.
Expand Down Expand Up @@ -193,15 +188,17 @@ func (o Opts) GoString() string {
// domain is the domain name of your website. It can be an exact domain, subdomain or wildcard.
// port is the TLS port where the server will listen on. Http requests will also redirected to that port.
//
// logger is an [slog.Logger] that will be used for logging. It is used in the server, it's use in middlewares is only if [logFunc] is nil.
//
// secretKey is used for securing signed data. It should be unique & kept secret.
// If it becomes compromised, generate a new one and restart your application using the new one.
//
// strategy is the algorithm to use when fetching the client's IP address; see [ClientIPstrategy].
// It is important to choose your strategy carefully, see the warning in [ClientIPstrategy].
//
// logger is an [slog.Logger] that will be used for logging.
//
// rateShedSamplePercent is the percentage of rate limited or loadshed responses that will be logged as errors. If it is less than 0, [DefaultRateShedSamplePercent] is used instead.
// logFunc is a function that dictates what/how middleware is going to log. It is also used to log any recovered panics in the middleware.
// This function is not used in the server.
// If it is nil, a suitable default(that utilizes [logger]) is used. To disable logging, use a function that does nothing.
//
// rateLimit is the maximum requests allowed (from one IP address) per second. If it is les than 1.0, [DefaultRateLimit] is used instead.
//
Expand Down Expand Up @@ -257,12 +254,12 @@ func New(
// common
domain string,
port uint16,
logger *slog.Logger,

// middleware
secretKey string,
strategy ClientIPstrategy,
logger *slog.Logger,
rateShedSamplePercent int,
logFunc func(w http.ResponseWriter, r http.Request, statusCode int, fields []any),
rateLimit float64,
loadShedSamplingPeriod time.Duration,
loadShedMinSampleSize int,
Expand Down Expand Up @@ -293,10 +290,10 @@ func New(
middlewareOpts, err := newMiddlewareOpts(
domain,
port,
logger,
secretKey,
strategy,
logger,
rateShedSamplePercent,
logFunc,
rateLimit,
loadShedSamplingPeriod,
loadShedMinSampleSize,
Expand Down Expand Up @@ -355,12 +352,12 @@ func WithOpts(
// common
domain,
httpsPort,
logger,

// middleware
secretKey,
strategy,
logger,
DefaultRateShedSamplePercent,
nil,
DefaultRateLimit,
DefaultLoadShedSamplingPeriod,
DefaultLoadShedMinSampleSize,
Expand Down Expand Up @@ -404,12 +401,12 @@ func DevOpts(logger *slog.Logger, secretKey string) Opts {
// common
domain,
httpsPort,
logger,

// middleware
secretKey,
clientip.DirectIpStrategy,
logger,
DefaultRateShedSamplePercent,
nil,
DefaultRateLimit,
DefaultLoadShedSamplingPeriod,
DefaultLoadShedMinSampleSize,
Expand Down Expand Up @@ -460,12 +457,12 @@ func CertOpts(
// common
domain,
httpsPort,
logger,

// middleware
secretKey,
clientip.DirectIpStrategy,
logger,
DefaultRateShedSamplePercent,
nil,
DefaultRateLimit,
DefaultLoadShedSamplingPeriod,
DefaultLoadShedMinSampleSize,
Expand Down Expand Up @@ -519,12 +516,12 @@ func AcmeOpts(
// common
domain,
httpsPort,
logger,

// middleware
secretKey,
clientip.DirectIpStrategy,
logger,
DefaultRateShedSamplePercent,
nil,
DefaultRateLimit,
DefaultLoadShedSamplingPeriod,
DefaultLoadShedMinSampleSize,
Expand Down Expand Up @@ -577,12 +574,12 @@ func LetsEncryptOpts(
// common
domain,
httpsPort,
logger,

// middleware
secretKey,
clientip.DirectIpStrategy,
logger,
DefaultRateShedSamplePercent,
nil,
DefaultRateLimit,
DefaultLoadShedSamplingPeriod,
DefaultLoadShedMinSampleSize,
Expand Down Expand Up @@ -632,16 +629,15 @@ func (s secureKey) GoString() string {
type middlewareOpts struct {
Domain string
HttpsPort uint16
Logger *slog.Logger

// When printing a struct, fmt does not invoke custom formatting methods on unexported fields.
// We thus need to make this field to be exported.
// - https://pkg.go.dev/fmt#:~:text=When%20printing%20a%20struct
// - https://go.dev/play/p/wL2gqumZ23b
SecretKey secureKey
Strategy ClientIPstrategy
Logger *slog.Logger

// logger
RateShedSamplePercent int
LogFunc func(w http.ResponseWriter, r http.Request, statusCode int, fields []any)

// ratelimit
RateLimit float64
Expand Down Expand Up @@ -673,8 +669,6 @@ func (m middlewareOpts) String() string {
HttpsPort: %d,
SecretKey: %s,
Strategy: %v,
Logger: %v,
RateShedSamplePercent: %v,
RateLimit: %v,
LoadShedSamplingPeriod: %v,
LoadShedMinSampleSize: %v,
Expand All @@ -692,8 +686,6 @@ func (m middlewareOpts) String() string {
m.HttpsPort,
m.SecretKey,
m.Strategy,
m.Logger,
m.RateShedSamplePercent,
m.RateLimit,
m.LoadShedSamplingPeriod,
m.LoadShedMinSampleSize,
Expand All @@ -717,10 +709,10 @@ func (m middlewareOpts) GoString() string {
func newMiddlewareOpts(
domain string,
httpsPort uint16,
logger *slog.Logger,
secretKey string,
strategy ClientIPstrategy,
logger *slog.Logger,
rateShedSamplePercent int,
logFunc func(w http.ResponseWriter, r http.Request, statusCode int, fields []any),
rateLimit float64,
loadShedSamplingPeriod time.Duration,
loadShedMinSampleSize int,
Expand All @@ -743,6 +735,10 @@ func newMiddlewareOpts(
domain = domain[2:]
}

if logger == nil && logFunc == nil {
return middlewareOpts{}, errors.New("both logger and logFunc should not be nil at the same time")
}

if err := key.IsSecure(secretKey); err != nil {
return middlewareOpts{}, err
}
Expand Down Expand Up @@ -771,12 +767,10 @@ func newMiddlewareOpts(
return middlewareOpts{
Domain: domain,
HttpsPort: httpsPort,
Logger: logger,
SecretKey: secureKey(secretKey),
Strategy: strategy,
Logger: logger,

// logger
RateShedSamplePercent: rateShedSamplePercent,
LogFunc: logFunc,

// ratelimiter
RateLimit: rateLimit,
Expand Down Expand Up @@ -1051,13 +1045,7 @@ func (o Opts) Equal(other Opts) bool {
if o.Strategy != other.Strategy {
return false
}
if o.Logger != other.Logger {
return false
}

if o.RateShedSamplePercent != other.RateShedSamplePercent {
return false
}
if int(o.RateLimit) != int(other.RateLimit) {
return false
}
Expand Down
26 changes: 14 additions & 12 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,17 @@ func validOpts(t *testing.T) Opts {
"example.com",
// The https port that our application will be listening on.
443,
// Logger.
l,
// The security key to use for securing signed data.
"super-h@rd-Pas1word",
// In this case, the actual client IP address is fetched from the given http header.
SingleIpStrategy("CF-Connecting-IP"),
// Logger.
l,
// log 90% of all responses that are either rate-limited or loadshed.
90,
// function to use for logging in middlewares.
func(_ http.ResponseWriter, r http.Request, statusCode int, fields []any) {
reqL := log.WithID(r.Context(), l)
reqL.Info("request-and-response", fields...)
},
// If a particular IP address sends more than 13 requests per second, throttle requests from that IP.
13.0,
// Sample response latencies over a 5 minute window to determine if to loadshed.
Expand Down Expand Up @@ -132,10 +135,10 @@ func TestNewMiddlewareOpts(t *testing.T) {
o, err := newMiddlewareOpts(
opt.Domain,
opt.HttpsPort,
slog.Default(),
string(opt.SecretKey),
opt.Strategy,
opt.Logger,
opt.RateShedSamplePercent,
nil,
opt.RateLimit,
opt.LoadShedSamplingPeriod,
opt.LoadShedMinSampleSize,
Expand Down Expand Up @@ -193,10 +196,10 @@ func TestNewMiddlewareOptsDomain(t *testing.T) {
_, err := newMiddlewareOpts(
tt.domain,
443,
slog.Default(),
tst.SecretKey(),
clientip.DirectIpStrategy,
slog.Default(),
DefaultRateShedSamplePercent,
nil,
DefaultRateLimit,
DefaultLoadShedSamplingPeriod,
DefaultLoadShedMinSampleSize,
Expand All @@ -215,10 +218,10 @@ func TestNewMiddlewareOptsDomain(t *testing.T) {
_, err := newMiddlewareOpts(
tt.domain,
443,
slog.Default(),
tst.SecretKey(),
clientip.DirectIpStrategy,
slog.Default(),
DefaultRateShedSamplePercent,
nil,
DefaultRateLimit,
DefaultLoadShedSamplingPeriod,
DefaultLoadShedMinSampleSize,
Expand Down Expand Up @@ -253,8 +256,7 @@ func TestOpts(t *testing.T) {
HttpsPort: 65081,
SecretKey: secureKey(tst.SecretKey()),
Strategy: clientip.DirectIpStrategy,
Logger: l,
RateShedSamplePercent: DefaultRateShedSamplePercent,
LogFunc: nil,
RateLimit: DefaultRateLimit,
LoadShedSamplingPeriod: DefaultLoadShedSamplingPeriod,
LoadShedMinSampleSize: DefaultLoadShedMinSampleSize,
Expand Down
15 changes: 11 additions & 4 deletions config/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,21 @@ func ExampleNew() {
"example.com",
// The https port that our application will be listening on.
443,
// Logger.
l,
// The security key to use for securing signed data.
"super-h@rd-Pas1word",
// In this case, the actual client IP address is fetched from the given http header.
config.SingleIpStrategy("CF-Connecting-IP"),
// Logger.
l,
// log 90% of all responses that are either rate-limited or loadshed.
90,
// function to use for logging in middlewares
func(_ http.ResponseWriter, r http.Request, statusCode int, fields []any) {
if statusCode >= http.StatusInternalServerError {
// Only log 500's
reqL := log.WithID(r.Context(), l)
fields = append(fields, "statusCode", statusCode)
reqL.Info("request-and-response", fields...)
}
},
// If a particular IP address sends more than 13 requests per second, throttle requests from that IP.
13.0,
// Sample response latencies over a 5 minute window to determine if to loadshed.
Expand Down
Loading
Loading