Repository: rollbar/rollbar-go
Pull request #114 (2025-04-16 05:11:11)

add support for global logger level and error levels
## Description of the change

1. Adding support for setting logger level globally
2. Allowing for error filters based on the error type and its corresponding level

example:

```go
rollbar.SetErrorLevelFilters(
		map[reflect.Type]string{
			reflect.TypeOf(rollbar.ErrBufferFull{}): rollbar.DEBUG,
			reflect.TypeOf(errors.New("")):          rollbar.ERR,
		},
	)  // setting ErrBufferFull to debug level and regular err type to error level
rollbar.SetLoggerLevel(rollbar.IGNORE) // sets global log level to ignore (meaning ignoring all errors and messages) 

```

the logic first checks for the global log level and then checks for the individual error levels. In the example above, `rollbar.SetErrorLevelFilters` does not have any affect because  `rollbar.SetLoggerLevel` sets the logger level to **IGNORE** - it will skip all the messages 


## Type of change

- [ ] Bug fix (non-breaking change that fixes an issue)
- [x] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Maintenance
- [ ] New release

## Checklists

### Development

- [ ] Lint rules pass locally
- [ ] The code changed/added as part of this pull request has been covered with tests
- [ ] All tests related to the changed code pass in development

### Code review

- [ ] This pull request has a descriptive title and information useful to a reviewer. There may be a screenshot or screencast attached
- [x] "Ready for review" label attached to the PR and reviewers assigned
- [ ] Issue from task tracker has a link to this pull request
- [ ] Changes have been reviewed by at least one other engineer



Patch:
diff --git a/async_transport.go b/async_transport.go
index 03a84f9..8d246c2 100644
--- a/async_transport.go
+++ b/async_transport.go
@@ -147,7 +147,9 @@ func (t *AsyncTransport) Send(body map[string]interface{}) (err error) {
 		}
 	} else {
 		err = ErrBufferFull{}
-		rollbarError(t.Logger, err.Error())
+		if !t.IsMessageFiltered(err, ERR) {
+			rollbarError(t.Logger, err.Error())
+		}
 		if t.PrintPayloadOnError {
 			writePayloadToStderr(t.Logger, body)
 		}
diff --git a/base_transport.go b/base_transport.go
index fb6a120..6afe6e1 100644
--- a/base_transport.go
+++ b/base_transport.go
@@ -7,6 +7,7 @@ import (
 	"io"
 	"io/ioutil"
 	"net/http"
+	"reflect"
 	"sync"
 	"time"
 )
@@ -29,6 +30,10 @@ type baseTransport struct {
 	PrintPayloadOnError bool
 	// ItemsPerMinute has the max number of items to send in a given minute
 	ItemsPerMinute int
+	// ErrorLevelFilters are the filtered error types and their corresponding levels
+	ErrorLevelFilters map[reflect.Type]string
+	// LoggerLevel is the logger level set globally
+	LoggerLevel string
 	// custom http client (http.DefaultClient used by default)
 	httpClient *http.Client
 
@@ -128,10 +133,13 @@ func (t *baseTransport) post(body map[string]interface{}) (bool, error) {
 	resp.Body.Close()
 
 	if resp.StatusCode != 200 {
-		rollbarError(t.Logger, "received response: %s", resp.Status)
+		err = ErrHTTPError(resp.StatusCode)
+		if !t.IsMessageFiltered(err, ERR) {
+			rollbarError(t.Logger, "received response: %s", resp.Status)
+		}
 		// http.StatusTooManyRequests is only defined in Go 1.6+ so we use 429 directly
 		isRateLimit := resp.StatusCode == 429
-		return isRateLimit, ErrHTTPError(resp.StatusCode)
+		return isRateLimit, err
 	}
 
 	return false, nil
@@ -147,3 +155,28 @@ func (t *baseTransport) shouldSend() bool {
 	}
 	return true
 }
+
+// SetLoggerLevel sets the logger level globally
+func (t *baseTransport) SetLoggerLevel(loggerLevel string) {
+	t.LoggerLevel = loggerLevel
+}
+
+// SetErrorLevelFilters sets error level filters
+func (t *baseTransport) SetErrorLevelFilters(errLevelFilters map[reflect.Type]string) {
+	t.ErrorLevelFilters = errLevelFilters
+}
+
+// IsMessageFiltered determines if the message should be filtered or not
+func (t *baseTransport) IsMessageFiltered(err interface{}, level string) bool {
+	if LogLevelMap[t.LoggerLevel] >= LogLevelMap[level] {
+		return true
+	}
+
+	for eType, lvl := range t.ErrorLevelFilters {
+		if eType == reflect.TypeOf(err) && LogLevelMap[lvl] >= LogLevelMap[level] {
+			return true
+		}
+	}
+
+	return false
+}
diff --git a/client.go b/client.go
index bb43c1c..8602405 100644
--- a/client.go
+++ b/client.go
@@ -118,6 +118,16 @@ func (c *Client) SetToken(token string) {
 	c.Transport.SetToken(token)
 }
 
+// SetLoggerLevel sets the logger level globally
+func (c *Client) SetLoggerLevel(loggerLevel string) {
+	c.Transport.SetLoggerLevel(loggerLevel)
+}
+
+// SetErrorLevelFilters sets the error level filters
+func (c *Client) SetErrorLevelFilters(errLevels map[reflect.Type]string) {
+	c.Transport.SetErrorLevelFilters(errLevels)
+}
+
 // SetEnvironment sets the environment under which all errors and messages will be submitted.
 func (c *Client) SetEnvironment(environment string) {
 	c.configuration.environment = environment
@@ -431,6 +441,9 @@ func (c *Client) ErrorWithStackSkipWithExtrasAndContext(ctx context.Context, lev
 	if !c.configuration.enabled {
 		return
 	}
+	if c.Transport.IsMessageFiltered(err, level) {
+		return
+	}
 	body := c.buildBody(ctx, level, err.Error(), extras)
 	telemetry := c.Telemetry.GetQueueItems()
 	addErrorToBody(c.configuration, body, err, skip, telemetry)
@@ -460,6 +473,9 @@ func (c *Client) RequestErrorWithStackSkipWithExtrasAndContext(ctx context.Conte
 	if !c.configuration.enabled {
 		return
 	}
+	if c.Transport.IsMessageFiltered(err, level) {
+		return
+	}
 	body := c.buildBody(ctx, level, err.Error(), extras)
 	telemetry := c.Telemetry.GetQueueItems()
 	data := addErrorToBody(c.configuration, body, err, skip, telemetry)
@@ -486,6 +502,9 @@ func (c *Client) MessageWithExtrasAndContext(ctx context.Context, level string,
 	if !c.configuration.enabled {
 		return
 	}
+	if c.Transport.IsMessageFiltered(nil, level) {
+		return
+	}
 	body := c.buildBody(ctx, level, msg, extras)
 	data := body["data"].(map[string]interface{})
 	dataBody := messageBody(msg)
@@ -514,6 +533,9 @@ func (c *Client) RequestMessageWithExtrasAndContext(ctx context.Context, level s
 	if !c.configuration.enabled {
 		return
 	}
+	if c.Transport.IsMessageFiltered(nil, level) {
+		return
+	}
 	body := c.buildBody(ctx, level, msg, extras)
 	data := body["data"].(map[string]interface{})
 	dataBody := messageBody(msg)
diff --git a/rollbar.go b/rollbar.go
index 66fab61..7df6246 100644
--- a/rollbar.go
+++ b/rollbar.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"net/http"
 	"os"
+	"reflect"
 	"regexp"
 	"runtime"
 )
@@ -13,7 +14,8 @@ const (
 	NAME = "rollbar/rollbar-go"
 	// VERSION is the version of this notifier sent with the payload to Rollbar.
 	VERSION = "1.2.0"
-
+	//IGNORE is to skip sending errors
+	IGNORE = "ignore"
 	// CRIT is the critial severity level.
 	CRIT = "critical"
 	// ERR is the error severity level.
@@ -30,6 +32,8 @@ const (
 	FILTERED = "[FILTERED]"
 )
 
+var LogLevelMap = map[string]int{DEBUG: 1, INFO: 2, WARN: 3, ERR: 4, CRIT: 5, IGNORE: 6}
+
 var (
 	hostname, _ = os.Hostname()
 	std         = NewAsync("", "development", "", hostname, "")
@@ -196,10 +200,12 @@ func SetScrubFields(fields *regexp.Regexp) {
 // SetTransform sets the transform function called after the entire payload has been built before it
 // is sent to the API.
 // The structure of the final payload sent to the API is:
-//   {
-//       "access_token": "YOUR_ACCESS_TOKEN",
-//       "data": { ... }
-//   }
+//
+//	{
+//	    "access_token": "YOUR_ACCESS_TOKEN",
+//	    "data": { ... }
+//	}
+//
 // This function takes a map[string]interface{} which is the value of the data key in the payload
 // described above. You can modify this object in-place to make any arbitrary changes you wish to
 // make before it is finally sent. Be careful with the modifications you make as they could lead to
@@ -357,11 +363,13 @@ func CaptureIp() captureIp {
 // -- Reporting
 
 // Critical reports an item with level `critical`. This function recognizes arguments with the following types:
-//    *http.Request
-//    error
-//    string
-//    map[string]interface{}
-//    int
+//
+//	*http.Request
+//	error
+//	string
+//	map[string]interface{}
+//	int
+//
 // The string and error types are mutually exclusive.
 // If an error is present then a stack trace is captured. If an int is also present then we skip
 // that number of stack frames. If the map is present it is used as extra custom data in the
@@ -372,11 +380,13 @@ func Critical(interfaces ...interface{}) {
 }
 
 // Error reports an item with level `error`. This function recognizes arguments with the following types:
-//    *http.Request
-//    error
-//    string
-//    map[string]interface{}
-//    int
+//
+//	*http.Request
+//	error
+//	string
+//	map[string]interface{}
+//	int
+//
 // The string and error types are mutually exclusive.
 // If an error is present then a stack trace is captured. If an int is also present then we skip
 // that number of stack frames. If the map is present it is used as extra custom data in the
@@ -387,11 +397,13 @@ func Error(interfaces ...interface{}) {
 }
 
 // Warning reports an item with level `warning`. This function recognizes arguments with the following types:
-//    *http.Request
-//    error
-//    string
-//    map[string]interface{}
-//    int
+//
+//	*http.Request
+//	error
+//	string
+//	map[string]interface{}
+//	int
+//
 // The string and error types are mutually exclusive.
 // If an error is present then a stack trace is captured. If an int is also present then we skip
 // that number of stack frames. If the map is present it is used as extra custom data in the
@@ -402,11 +414,13 @@ func Warning(interfaces ...interface{}) {
 }
 
 // Info reports an item with level `info`. This function recognizes arguments with the following types:
-//    *http.Request
-//    error
-//    string
-//    map[string]interface{}
-//    int
+//
+//	*http.Request
+//	error
+//	string
+//	map[string]interface{}
+//	int
+//
 // The string and error types are mutually exclusive.
 // If an error is present then a stack trace is captured. If an int is also present then we skip
 // that number of stack frames. If the map is present it is used as extra custom data in the
@@ -417,11 +431,13 @@ func Info(interfaces ...interface{}) {
 }
 
 // Debug reports an item with level `debug`. This function recognizes arguments with the following types:
-//    *http.Request
-//    error
-//    string
-//    map[string]interface{}
-//    int
+//
+//	*http.Request
+//	error
+//	string
+//	map[string]interface{}
+//	int
+//
 // The string and error types are mutually exclusive.
 // If an error is present then a stack trace is captured. If an int is also present then we skip
 // that number of stack frames. If the map is present it is used as extra custom data in the
@@ -432,12 +448,14 @@ func Debug(interfaces ...interface{}) {
 }
 
 // Log reports an item with the given level. This function recognizes arguments with the following types:
-//    *http.Request
-//    error
-//    string
-//    map[string]interface{}
-//    int
-//    context.Context
+//
+//	*http.Request
+//	error
+//	string
+//	map[string]interface{}
+//	int
+//	context.Context
+//
 // The string and error types are mutually exclusive.
 // If an error is present then a stack trace is captured. If an int is also present then we skip
 // that number of stack frames. If the map is present it is used as extra custom data in the
@@ -656,6 +674,16 @@ func LambdaWrapper(handlerFunc interface{}) interface{} {
 	return std.LambdaWrapper(handlerFunc)
 }
 
+// SetErrorLevelFilters sets filtered error types and their corresponding levels
+func SetErrorLevelFilters(errLevels map[reflect.Type]string) {
+	std.SetErrorLevelFilters(errLevels)
+}
+
+// SetLoggerLevel sets logger level globally
+func SetLoggerLevel(loggerLevel string) {
+	std.SetLoggerLevel(loggerLevel)
+}
+
 // Stacker is an interface that errors can implement to allow the extraction of stack traces.
 // To generate a stack trace, users are required to call runtime.Callers and build the runtime.Frame slice
 // at the time the error is created.
diff --git a/transport.go b/transport.go
index 0133cb4..50cddd3 100644
--- a/transport.go
+++ b/transport.go
@@ -7,6 +7,7 @@ import (
 	"log"
 	"net/http"
 	"os"
+	"reflect"
 )
 
 const (
@@ -53,6 +54,12 @@ type Transport interface {
 	SetItemsPerMinute(itemsPerMinute int)
 	// SetContext sets the context to use for API calls made over the Transport
 	SetContext(ctx context.Context)
+	// SetErrorLevelFilters sets errors and their corresponding levels
+	SetErrorLevelFilters(errLevels map[reflect.Type]string)
+	// IsMessageFiltered returns true if the message is filtered out
+	IsMessageFiltered(err interface{}, level string) bool
+	// SetLoggerLevel sets logger level globally
+	SetLoggerLevel(loggerLevel string)
 }
 
 // ClientLogger is the interface used by the rollbar Client/Transport to report problems.


Test patch:
diff --git a/client_test.go b/client_test.go
index 4027606..e0156a7 100644
--- a/client_test.go
+++ b/client_test.go
@@ -26,13 +26,19 @@ func (t *TestTransport) Wait() {
 func (t *TestTransport) SetContext(ctx context.Context) {
 }
 
-func (t *TestTransport) SetToken(_t string)             {}
-func (t *TestTransport) SetEndpoint(_e string)          {}
-func (t *TestTransport) SetLogger(_l ClientLogger)      {}
-func (t *TestTransport) SetRetryAttempts(_r int)        {}
-func (t *TestTransport) SetPrintPayloadOnError(_p bool) {}
-func (t *TestTransport) SetHTTPClient(_c *http.Client)  {}
-func (t *TestTransport) SetItemsPerMinute(_r int)       {}
+func (t *TestTransport) SetToken(_t string)                                 {}
+func (t *TestTransport) SetEndpoint(_e string)                              {}
+func (t *TestTransport) SetLogger(_l ClientLogger)                          {}
+func (t *TestTransport) SetRetryAttempts(_r int)                            {}
+func (t *TestTransport) SetPrintPayloadOnError(_p bool)                     {}
+func (t *TestTransport) SetHTTPClient(_c *http.Client)                      {}
+func (t *TestTransport) SetItemsPerMinute(_r int)                           {}
+func (t *TestTransport) SetErrorLevelFilters(_errs map[reflect.Type]string) {}
+func (t *TestTransport) SetLoggerLevel(_l string)                           {}
+func (t *TestTransport) IsMessageFiltered(_e interface{}, _l string) bool {
+	return false
+}
+
 func (t *TestTransport) Send(body map[string]interface{}) error {
 	t.Body = body
 	return nil
@@ -469,6 +475,8 @@ func testGettersAndSetters(client *Client, t *testing.T) {
 	client.SetScrubFields(scrubFields)
 	client.SetCaptureIp(captureIP)
 	client.SetTelemetry()
+	client.SetLoggerLevel(DEBUG)
+	client.SetErrorLevelFilters(map[reflect.Type]string{reflect.TypeOf(ErrBufferFull{}): DEBUG, reflect.TypeOf(errors.New("")): IGNORE})
 
 	client.SetEnabled(true)
 	client.SetItemsPerMinute(itemsPerMinute)
diff --git a/rollbar_test.go b/rollbar_test.go
index 84c0ed5..dbaacc0 100644
--- a/rollbar_test.go
+++ b/rollbar_test.go
@@ -7,6 +7,7 @@ import (
 	"io/ioutil"
 	"net/http"
 	"os"
+	"reflect"
 	"runtime"
 	"strings"
 	"testing"
@@ -676,6 +677,34 @@ func TestNewAsyncWithContext(t *testing.T) {
 		t.Error("Transport ctx must be properly set")
 	}
 }
+
+func TestLoggerLevel(t *testing.T) {
+	ctx, _ := context.WithTimeout(context.Background(), 4*time.Second)
+	client := NewAsync("example", "test", "0.0.0", "", "", WithClientContext(ctx))
+	if client.ctx != ctx {
+		t.Error("Client ctx must be properly set")
+	}
+	filters := map[reflect.Type]string{reflect.TypeOf(ErrBufferFull{}): DEBUG, reflect.TypeOf(errors.New("")): IGNORE}
+	client.SetLoggerLevel(INFO)
+	client.SetErrorLevelFilters(filters)
+	tr := client.Transport.(*AsyncTransport)
+	if tr.LoggerLevel != INFO {
+		t.Error("logger level must be INFO")
+	}
+
+	if !tr.IsMessageFiltered(errors.New(""), DEBUG) { // global filtering
+		t.Error("error must be filtered out")
+	}
+	if !tr.IsMessageFiltered(errors.New(""), WARN) { // specific error filtering
+		t.Error("error must be filtered out")
+	}
+	filters = map[reflect.Type]string{reflect.TypeOf(ErrBufferFull{}): DEBUG}
+	client.SetErrorLevelFilters(filters)
+	if tr.IsMessageFiltered(errors.New(""), WARN) { // specific error filtering
+		t.Error("error must be ok to send")
+	}
+}
+
 func TestSetHttpClient(t *testing.T) {
 	used := false
 	c := &http.Client{