Skip to content

Commit

Permalink
Give users control over what and how logging happens in the middlewar…
Browse files Browse the repository at this point in the history
…es (#472)

- Users can be able to disable logging completely in the middleware by using a `logFunc` that does nothing.
  It is not yet possible to disable logging in the server, the main issue to solve is what to do with `http.Server.Errorlog`; https://github.com/komuw/ong/blob/v0.1.6/server/server.go#L127. This can be solved in the future.
  • Loading branch information
komuw authored Aug 17, 2024
1 parent dfa10a2 commit 1d2b1f1
Show file tree
Hide file tree
Showing 10 changed files with 175 additions and 130 deletions.
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

0 comments on commit 1d2b1f1

Please sign in to comment.