-
Notifications
You must be signed in to change notification settings - Fork 92
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
Rate limited errors #195
base: master
Are you sure you want to change the base?
Rate limited errors #195
Changes from 2 commits
b0e42ef
6427096
0fa1c6c
bc9ae8d
db20483
54c934b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
package logging | ||
|
||
import "golang.org/x/time/rate" | ||
|
||
type rateLimitedLogger struct { | ||
next Interface | ||
limiter *rate.Limiter | ||
} | ||
|
||
// NewRateLimitedLogger returns a logger.Interface that is limited to a number | ||
// of logs per second | ||
func NewRateLimitedLogger(logger Interface, logsPerSecond rate.Limit) Interface { | ||
return &rateLimitedLogger{ | ||
next: logger, | ||
limiter: rate.NewLimiter(logsPerSecond, 1), | ||
} | ||
} | ||
|
||
func (l *rateLimitedLogger) Debugf(format string, args ...interface{}) { | ||
if l.limiter.Allow() { | ||
l.next.Debugf(format, args...) | ||
} | ||
} | ||
|
||
func (l *rateLimitedLogger) Debugln(args ...interface{}) { | ||
if l.limiter.Allow() { | ||
l.next.Debugln(args...) | ||
} | ||
} | ||
|
||
func (l *rateLimitedLogger) Infof(format string, args ...interface{}) { | ||
if l.limiter.Allow() { | ||
l.next.Infof(format, args...) | ||
} | ||
} | ||
|
||
func (l *rateLimitedLogger) Infoln(args ...interface{}) { | ||
if l.limiter.Allow() { | ||
l.next.Infoln(args...) | ||
} | ||
} | ||
|
||
func (l *rateLimitedLogger) Errorf(format string, args ...interface{}) { | ||
if l.limiter.Allow() { | ||
l.next.Errorf(format, args...) | ||
} | ||
} | ||
|
||
func (l *rateLimitedLogger) Errorln(args ...interface{}) { | ||
if l.limiter.Allow() { | ||
l.next.Errorln(args...) | ||
} | ||
} | ||
|
||
func (l *rateLimitedLogger) Warnf(format string, args ...interface{}) { | ||
if l.limiter.Allow() { | ||
l.next.Warnf(format, args...) | ||
} | ||
} | ||
|
||
func (l *rateLimitedLogger) Warnln(args ...interface{}) { | ||
if l.limiter.Allow() { | ||
l.next.Warnln(args...) | ||
} | ||
} | ||
|
||
func (l *rateLimitedLogger) WithField(key string, value interface{}) Interface { | ||
return &rateLimitedLogger{ | ||
next: l.next.WithField(key, value), | ||
limiter: rate.NewLimiter(l.limiter.Limit(), 0), | ||
} | ||
} | ||
|
||
func (l *rateLimitedLogger) WithFields(f Fields) Interface { | ||
return &rateLimitedLogger{ | ||
next: l.next.WithFields(f), | ||
limiter: rate.NewLimiter(l.limiter.Limit(), 0), | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
package logging | ||
|
||
import ( | ||
"testing" | ||
|
||
"gotest.tools/assert" | ||
) | ||
|
||
type counterLogger struct { | ||
count int | ||
} | ||
|
||
func (c *counterLogger) Debugf(format string, args ...interface{}) { c.count++ } | ||
func (c *counterLogger) Debugln(args ...interface{}) { c.count++ } | ||
func (c *counterLogger) Infof(format string, args ...interface{}) { c.count++ } | ||
func (c *counterLogger) Infoln(args ...interface{}) { c.count++ } | ||
func (c *counterLogger) Warnf(format string, args ...interface{}) { c.count++ } | ||
func (c *counterLogger) Warnln(args ...interface{}) { c.count++ } | ||
func (c *counterLogger) Errorf(format string, args ...interface{}) { c.count++ } | ||
func (c *counterLogger) Errorln(args ...interface{}) { c.count++ } | ||
func (c *counterLogger) WithField(key string, value interface{}) Interface { | ||
return c | ||
} | ||
func (c *counterLogger) WithFields(Fields) Interface { | ||
return c | ||
} | ||
|
||
func TestRateLimitedLoggerLogs(t *testing.T) { | ||
c := &counterLogger{} | ||
r := NewRateLimitedLogger(c, 1) | ||
|
||
r.Errorln("asdf") | ||
assert.Equal(t, 1, c.count) | ||
} | ||
|
||
func TestRateLimitedLoggerLimits(t *testing.T) { | ||
c := &counterLogger{} | ||
r := NewRateLimitedLogger(c, 1) | ||
|
||
r.Errorln("asdf") | ||
r.Infoln("asdf") | ||
assert.Equal(t, 1, c.count) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,18 +13,23 @@ import ( | |
|
||
// Log middleware logs http requests | ||
type Log struct { | ||
Log logging.Interface | ||
LogRequestHeaders bool // LogRequestHeaders true -> dump http headers at debug log level | ||
Log logging.Interface | ||
HighVolumeErrorLog logging.Interface | ||
LogRequestHeaders bool // LogRequestHeaders true -> dump http headers at debug log level | ||
} | ||
|
||
// logWithRequest information from the request and context as fields. | ||
func (l Log) logWithRequest(r *http.Request) logging.Interface { | ||
return l.logWithRequestAndLog(r, l.Log) | ||
} | ||
|
||
func (l Log) logWithRequestAndLog(r *http.Request, logger logging.Interface) logging.Interface { | ||
traceID, ok := ExtractTraceID(r.Context()) | ||
if ok { | ||
l.Log = l.Log.WithField("traceID", traceID) | ||
logger = logger.WithField("traceID", traceID) | ||
} | ||
|
||
return user.LogWith(r.Context(), l.Log) | ||
return user.LogWith(r.Context(), logger) | ||
} | ||
|
||
// Wrap implements Middleware | ||
|
@@ -53,18 +58,31 @@ func (l Log) Wrap(next http.Handler) http.Handler { | |
|
||
return | ||
} | ||
if 100 <= statusCode && statusCode < 500 || statusCode == http.StatusBadGateway || statusCode == http.StatusServiceUnavailable { | ||
if 100 <= statusCode && statusCode < 500 { | ||
l.logWithRequest(r).Debugf("%s %s (%d) %s", r.Method, uri, statusCode, time.Since(begin)) | ||
if l.LogRequestHeaders && headers != nil { | ||
l.logWithRequest(r).Debugf("ws: %v; %s", IsWSHandshakeRequest(r), string(headers)) | ||
} | ||
} else if statusCode == http.StatusBadGateway || statusCode == http.StatusServiceUnavailable { | ||
l.logHighVolumeError(r, "%s %s (%d) %s", r.Method, uri, statusCode, time.Since(begin)) | ||
if l.LogRequestHeaders && headers != nil { | ||
l.logHighVolumeError(r, "ws: %v; %s", IsWSHandshakeRequest(r), string(headers)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do you think we want this on a different line to the method? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i think it's ok. this way the user can configure There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What I'm thinking is: suppose you want to look at logs for a specific request; you can grep for the URI. But the headers are on the next line, which is awkward. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. oh, that's a good point. i hadn't considered that. i'll push a small change to unify them |
||
} | ||
} else { | ||
l.logWithRequest(r).Warnf("%s %s (%d) %s Response: %q ws: %v; %s", | ||
r.Method, uri, statusCode, time.Since(begin), buf.Bytes(), IsWSHandshakeRequest(r), headers) | ||
} | ||
}) | ||
} | ||
|
||
func (l Log) logHighVolumeError(r *http.Request, format string, args ...interface{}) { | ||
if l.HighVolumeErrorLog != nil { | ||
l.logWithRequestAndLog(r, l.HighVolumeErrorLog).Warnf(format, args...) | ||
} else { | ||
l.logWithRequest(r).Debugf(format, args...) | ||
} | ||
} | ||
|
||
// Logging middleware logs each HTTP request method, path, response code and | ||
// duration for all HTTP requests. | ||
var Logging = Log{ | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we get a new limiter here?
Also, does it work with a burst size of zero?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, the burst limit was wrong. Good catch. Added a test and fixed.
As far as the new limiter I figured calling
WithFields()
created a new logger that should have an independent rate limit, but I can go either way on this.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thing is, code often calls
WithFields()
on every operation, e.g. the example you gave in the description, or this one:common/middleware/grpc_logging.go
Line 30 in 61ffdd4
I expect the case you're trying to hit is where a high volume of logs from from lots of operations, so you would want the same rate-limit to be applied across them all. Try it!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right. Thanks man 👍
Fixed.