Repository: rollbar/rollbar-go (go)

Issue:
Make Log a method of Client
This a feature request.

Quite happy with Rollbar and its Go library, but we do miss the option to have `rollbar.Log` available to use with any client and not only the default one.

As it is right now we have to wrap rollbar's clients in our own struct that implements a copy of `rollbar.Log` to emulate the behaviour around requests and error attributes.

This would be ideal:

```go
import "github.com/rollbar/rollbar-go"

rb_client := rollbar.New(...)
// stuff happens
rb_client.Log(error, request, ...)
```

Pull request description:
adding log capability to client
## Description of the change
Making logging capability on the client . 

Now sending messages works on both package (rollbar) and client levels.  
example: 
```go
import "github.com/rollbar/rollbar-go"

rb_client := rollbar.New(...)
// stuff happens
rb_client.Log("error", err, request, ...)
// or 
rb_client.Error(err, request, ...)

```

## 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

## Related issues

- Fix #110

## Checklists

### Development

- [x] Lint rules pass locally
- [x] The code changed/added as part of this pull request has been covered with tests
- [x] 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/client.go b/client.go
index 8602405..33f9355 100644
--- a/client.go
+++ b/client.go
@@ -80,6 +80,153 @@ func NewSync(token, environment, codeVersion, serverHost, serverRoot string) *Cl
 	}
 }
 
+// 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
+//
+// 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
+// item. If a string is present without an error, then we log a message without a stack
+// trace. If a request is present we extract as much relevant information from it as we can. If
+// a context is present, it is applied to downstream operations.
+func (c *Client) Log(level string, interfaces ...interface{}) {
+	var r *http.Request
+	var err error
+	var skip int
+	skipSet := false
+	var extras map[string]interface{}
+	var msg string
+	ctx := context.TODO()
+	for _, ival := range interfaces {
+		switch val := ival.(type) {
+		case *http.Request:
+			r = val
+		case error:
+			err = val
+		case int:
+			skip = val
+			skipSet = true
+		case string:
+			msg = val
+		case map[string]interface{}:
+			extras = val
+		case context.Context:
+			ctx = val
+		default:
+			rollbarError(c.Transport.(*AsyncTransport).Logger, "Unknown input type: %T", val)
+		}
+	}
+	if !skipSet {
+		skip = 2
+	}
+	if err != nil {
+		if r == nil {
+			c.ErrorWithStackSkipWithExtrasAndContext(ctx, level, err, skip, extras)
+		} else {
+			c.RequestErrorWithStackSkipWithExtrasAndContext(ctx, level, r, err, skip, extras)
+		}
+	} else {
+		if r == nil {
+			c.MessageWithExtrasAndContext(ctx, level, msg, extras)
+		} else {
+			c.RequestMessageWithExtrasAndContext(ctx, level, r, msg, extras)
+		}
+	}
+}
+
+// -- Reporting
+
+// Critical reports an item with level `critical`. This function recognizes arguments with the following types:
+//
+//	*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
+// item. If a string is present without an error, then we log a message without a stack
+// trace. If a request is present we extract as much relevant information from it as we can.
+func (c *Client) Critical(interfaces ...interface{}) {
+	c.Log(CRIT, interfaces...)
+}
+
+// Error reports an item with level `error`. This function recognizes arguments with the following types:
+//
+//	*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
+// item. If a string is present without an error, then we log a message without a stack
+// trace. If a request is present we extract as much relevant information from it as we can.
+func (c *Client) Error(interfaces ...interface{}) {
+	c.Log(ERR, interfaces...)
+}
+
+// Warning reports an item with level `warning`. This function recognizes arguments with the following types:
+//
+//	*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
+// item. If a string is present without an error, then we log a message without a stack
+// trace. If a request is present we extract as much relevant information from it as we can.
+func (c *Client) Warning(interfaces ...interface{}) {
+	c.Log(WARN, interfaces...)
+}
+
+// Info reports an item with level `info`. This function recognizes arguments with the following types:
+//
+//	*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
+// item. If a string is present without an error, then we log a message without a stack
+// trace. If a request is present we extract as much relevant information from it as we can.
+func (c *Client) Info(interfaces ...interface{}) {
+	c.Log(INFO, interfaces...)
+}
+
+// Debug reports an item with level `debug`. This function recognizes arguments with the following types:
+//
+//	*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
+// item. If a string is present without an error, then we log a message without a stack
+// trace. If a request is present we extract as much relevant information from it as we can.
+func (c *Client) Debug(interfaces ...interface{}) {
+	c.Log(DEBUG, interfaces...)
+}
+
 // CaptureTelemetryEvent sets the user-specified telemetry event
 func (c *Client) CaptureTelemetryEvent(eventType, eventlevel string, eventData map[string]interface{}) {
 	data := map[string]interface{}{}
diff --git a/rollbar.go b/rollbar.go
index 7df6246..8e47bb6 100644
--- a/rollbar.go
+++ b/rollbar.go
@@ -463,48 +463,7 @@ func Debug(interfaces ...interface{}) {
 // trace. If a request is present we extract as much relevant information from it as we can. If
 // a context is present, it is applied to downstream operations.
 func Log(level string, interfaces ...interface{}) {
-	var r *http.Request
-	var err error
-	var skip int
-	skipSet := false
-	var extras map[string]interface{}
-	var msg string
-	ctx := context.TODO()
-	for _, ival := range interfaces {
-		switch val := ival.(type) {
-		case *http.Request:
-			r = val
-		case error:
-			err = val
-		case int:
-			skip = val
-			skipSet = true
-		case string:
-			msg = val
-		case map[string]interface{}:
-			extras = val
-		case context.Context:
-			ctx = val
-		default:
-			rollbarError(std.Transport.(*AsyncTransport).Logger, "Unknown input type: %T", val)
-		}
-	}
-	if !skipSet {
-		skip = 2
-	}
-	if err != nil {
-		if r == nil {
-			std.ErrorWithStackSkipWithExtrasAndContext(ctx, level, err, skip, extras)
-		} else {
-			std.RequestErrorWithStackSkipWithExtrasAndContext(ctx, level, r, err, skip, extras)
-		}
-	} else {
-		if r == nil {
-			std.MessageWithExtrasAndContext(ctx, level, msg, extras)
-		} else {
-			std.RequestMessageWithExtrasAndContext(ctx, level, r, msg, extras)
-		}
-	}
+	std.Log(level, interfaces...)
 }
 
 // -- Error reporting


Test patch:
diff --git a/client_test.go b/client_test.go
index e0156a7..e8bbf16 100644
--- a/client_test.go
+++ b/client_test.go
@@ -72,6 +72,138 @@ func TestLogPanic(t *testing.T) {
 	client.Close()
 }
 
+func TestLogError(t *testing.T) {
+	client := testClient()
+	client.Error(errors.New("logged error"))
+	if transport, ok := client.Transport.(*TestTransport); ok {
+		if transport.WaitCalled {
+			t.Error("Wait called unexpectedly")
+		}
+		body := transport.Body
+		if body["data"] == nil {
+			t.Error("body should have data")
+		}
+		data := body["data"].(map[string]interface{})
+		dataError := errorFromData(data)
+		if dataError["message"] != "logged error" {
+			t.Error("data should have correct error message")
+		}
+	} else {
+		t.Fail()
+	}
+	client.Close()
+}
+
+func TestLogCriticalLevel(t *testing.T) {
+	client := testClient()
+	client.Critical(errors.New("logged error"))
+	if transport, ok := client.Transport.(*TestTransport); ok {
+		if transport.WaitCalled {
+			t.Error("Wait called unexpectedly")
+		}
+		body := transport.Body
+		if body["data"] == nil {
+			t.Error("body should have data")
+		}
+		data := body["data"].(map[string]interface{})
+
+		if data["level"] != CRIT {
+			t.Error("data should have correct level")
+		}
+	} else {
+		t.Fail()
+	}
+	client.Close()
+}
+
+func TestLogErrorLevel(t *testing.T) {
+	client := testClient()
+	client.Error(errors.New("logged error"))
+	if transport, ok := client.Transport.(*TestTransport); ok {
+		if transport.WaitCalled {
+			t.Error("Wait called unexpectedly")
+		}
+		body := transport.Body
+		if body["data"] == nil {
+			t.Error("body should have data")
+		}
+		data := body["data"].(map[string]interface{})
+
+		if data["level"] != ERR {
+			t.Error("data should have correct level")
+		}
+	} else {
+		t.Fail()
+	}
+	client.Close()
+}
+
+func TestLogWarningLevel(t *testing.T) {
+	client := testClient()
+	client.Warning(errors.New("logged error"))
+	if transport, ok := client.Transport.(*TestTransport); ok {
+		if transport.WaitCalled {
+			t.Error("Wait called unexpectedly")
+		}
+		body := transport.Body
+		if body["data"] == nil {
+			t.Error("body should have data")
+		}
+		data := body["data"].(map[string]interface{})
+
+		if data["level"] != WARN {
+			t.Error("data should have correct level")
+		}
+	} else {
+		t.Fail()
+	}
+	client.Close()
+}
+
+func TestLogInfoLevel(t *testing.T) {
+	client := testClient()
+	client.Info("some message")
+	if transport, ok := client.Transport.(*TestTransport); ok {
+		if transport.WaitCalled {
+			t.Error("Wait called unexpectedly")
+		}
+		body := transport.Body
+		if body["data"] == nil {
+			t.Error("body should have data")
+		}
+		data := body["data"].(map[string]interface{})
+
+		if data["level"] != INFO {
+			t.Error("data should have correct level")
+		}
+	} else {
+		t.Fail()
+	}
+	client.Close()
+}
+
+func TestLogDebugLevel(t *testing.T) {
+	client := testClient()
+	client.Debug("some message")
+	if transport, ok := client.Transport.(*TestTransport); ok {
+		if transport.WaitCalled {
+			t.Error("Wait called unexpectedly")
+		}
+		body := transport.Body
+		if body["data"] == nil {
+			t.Error("body should have data")
+		}
+		data := body["data"].(map[string]interface{})
+
+		if data["level"] != DEBUG {
+			t.Error("data should have correct level")
+		}
+	} else {
+		t.Fail()
+	}
+	client.Close()
+}
+
 func TestWrap(t *testing.T) {
 	client := testClient()
 	err := errors.New("bork")