Skip to content

Commit

Permalink
Reduce default verbosity of logs produced by default Client (#14)
Browse files Browse the repository at this point in the history
`retryablehttp.Client` logs excessively (one `Printf` to stdout per HTTP
request) unless a `retryablehttp.LeveledLogger` is configured as its
logger. Add a logger that implements `retryablehttp.LeveledLogger`,
whose verbosity defaults to `INFO` and can be controlled via the
`VERBOSITY` environment variable, and use it as the logger for the
default `Client`.

The logger increases the level of some messages produced by
`retryablehttp.Client`, in particular those that report when requests
are about to be retried, for the sake of visibility.
  • Loading branch information
chrisnovakovic authored Mar 13, 2024
1 parent 7e804f8 commit 99b9dae
Show file tree
Hide file tree
Showing 2 changed files with 125 additions and 4 deletions.
15 changes: 11 additions & 4 deletions jira.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,20 @@ type Client struct {
// Both the Jira REST API and retryablehttp.DefaultRetryPolicy implement RFC 6585 section 4, in
// which the server responds with status code 429 if too many requests have been sent and the
// client backs off for at least the number of seconds given in the Retry-After response header
// before retrying; this means that a retryablehttp.Client with default settings is all that is
// needed to obey Jira's API rate limits.
var defaultClient = retryablehttp.NewClient()
// before retrying; this means that a retryablehttp.Client will obey Jira's API rate limits out of
// the box.
var defaultClient *retryablehttp.Client

// defaultTransport is the underlying Transport used by the other Transports in this package if no
// other Transport is specified. It ensures that failed requests are automatically retried.
var defaultTransport = &retryablehttp.RoundTripper{Client: defaultClient}
var defaultTransport *retryablehttp.RoundTripper

func init() {
defaultClient = retryablehttp.NewClient()
defaultClient.Logger = logger

defaultTransport = &retryablehttp.RoundTripper{Client: defaultClient}
}

// NewClient returns a new Jira API client.
// If a nil httpClient is provided, a retryablehttp.Client with default settings will be used; this
Expand Down
114 changes: 114 additions & 0 deletions log.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package jira

import (
"context"
"io"
"log/slog"
"os"
)

const (
levelTrace = slog.Level(-8)
levelDebug = slog.LevelDebug
levelInfo = slog.LevelInfo
levelWarning = slog.LevelWarn
levelError = slog.LevelError
levelPanic = slog.Level(12)
levelFatal = slog.Level(16)

// logLevelEnvKey is the name of the environment variable whose value defines the logging level of
// the logger used by the default client.
logLevelEnvKey = "VERBOSITY"
)

// logger is the logger used by the default client. It writes log messages to stderr.
var logger = slog.New(newRetryablehttpHandler(os.Stderr))

// retryablehttpHandler handles log messages produced for LeveledLoggers by go-retryablehttp. It
// wraps another handler, mutating the log records it receives before forwarding them on to the
// wrapped handler.
type retryablehttpHandler struct {
// handler is the wrapped handler.
handler slog.Handler

// level is the minimum level for which messages should be logged.
level slog.Level
}

// newRetryablehttpHandler creates a retryablehttpHandler whose minimum logging level is the value
// of the VERBOSITY environment variable.
func newRetryablehttpHandler(output io.Writer) *retryablehttpHandler {
return &retryablehttpHandler{
handler: slog.NewTextHandler(output, nil),
level: defaultLogLevel(),
}
}

// Enabled reports whether the handler handles records at the given level. The handler ignores
// records whose level is lower.
//
// Enabled is called before Handle for performance reasons. Because retryablehttpHandler changes the
// level of some messages in Handle, records cannot be rejected at this point solely because of
// their level, so Enabled always returns true. However, Handle will only forward them on to the
// wrapped handler if their level is at least h.level.
func (h *retryablehttpHandler) Enabled(_ context.Context, _ slog.Level) bool {
return true
}

// WithAttrs returns a new retryablehttpHandler whose attributes consists of h's attributes followed
// by attrs.
func (h *retryablehttpHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
return &retryablehttpHandler{
handler: h.handler.WithAttrs(attrs),
level: h.level,
}
}

// WithGroup returns a new retryablehttpHandler with the given group appended to the receiver's
// existing groups.
func (h *retryablehttpHandler) WithGroup(name string) slog.Handler {
return &retryablehttpHandler{
handler: h.handler.WithGroup(name),
level: h.level,
}
}

// Handle processes a record created by a retryablehttp.Client and potentially forwards it on to the
// wrapped handler.
//
// Handle mutates records as follows:
// - records with a "retrying request" message (which are created by retryablehttp.Client when a
// request fails due to a server-side error or a retryable client-side error, e.g. when the Jira
// API's rate limits have been exceeded) are increased from debug level to warning level, to make
// them more visible.
func (h *retryablehttpHandler) Handle(ctx context.Context, r slog.Record) error {
if r.Message == "retrying request" {
r = r.Clone()
r.Level = levelWarning
}
if r.Level < h.level {
return nil
}
return h.handler.Handle(ctx, r)
}

func defaultLogLevel() slog.Level {
switch os.Getenv(logLevelEnvKey) {
case "TRACE":
return levelTrace
case "DEBUG":
return levelDebug
case "INFO":
return levelInfo
case "WARNING":
return levelWarning
case "ERROR":
return levelError
case "PANIC":
return levelPanic
case "FATAL":
return levelFatal
default:
return levelInfo
}
}

0 comments on commit 99b9dae

Please sign in to comment.