Skip to content
Draft
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ all: test
prepare:
# needed for `make fmt`
go get golang.org/x/tools/cmd/goimports
# linters
go get github.com/alecthomas/gometalinter
gometalinter --install
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
# needed for `make cover`
go get golang.org/x/tools/cmd/cover
@echo Now you should be ready to run "make"
Expand All @@ -18,7 +16,7 @@ fmt:
find . -name "*.go" -exec goimports -w {} \;

lint:
gometalinter
golangci-lint run

cover:
go test -cover -coverprofile cover.out
Expand Down
55 changes: 55 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,11 @@ The following options are available to you:
| Timeout | `time.Duration` | 3 seconds | `10 * time.Second` | `HONEYBADGER_TIMEOUT` (nanoseconds) |
| Logger | `honeybadger.Logger` | Logs to stderr | `CustomLogger{}` | n/a |
| Backend | `honeybadger.Backend` | HTTP backend | `CustomBackend{}` | n/a |
| EventsBatchSize | `int` | 1000 | `500` | n/a |
| EventsTimeout | `time.Duration` | 30 seconds | `10 * time.Second` | n/a |
| EventsMaxQueueSize | `int` | 100000 | `50000` | n/a |
| EventsMaxRetries | `int` | 3 | `5` | n/a |
| EventsThrottleWait | `time.Duration` | 60 seconds | `30 * time.Second` | n/a |


## Public Interface
Expand Down Expand Up @@ -228,6 +233,56 @@ honeybadger.BeforeNotify(

---

### `honeybadger.Event()`: Send events to Honeybadger Insights.

Send custom events to Honeybadger Insights for tracking application behavior and metrics.

#### Examples:

```go
honeybadger.Event("user_login", map[string]any{
"user_id": 123,
"email": "[email protected]",
})
```

Events are batched and sent asynchronously. Configuration options are available
for batching, retries, and throttling. See [Configuration](#configuration) for details.

---

### `honeybadger.BeforeEvent()`: Add a callback to skip or modify event data.

Similar to `BeforeNotify()`, you can add callbacks to modify event data or skip events entirely before they are sent.

#### Examples:

To modify or augment event data:

```go
honeybadger.BeforeEvent(
func(event map[string]any) error {
event["environment"] = "production"
return nil
}
)
```

To skip events, use `honeybadger.ErrEventDropped`:

```go
honeybadger.BeforeEvent(
func(event map[string]any) error {
if event["event_type"] == "debug_event" {
return honeybadger.ErrEventDropped
}
return nil
}
)
```

---

### ``honeybadger.NewNullBackend()``: Disable data reporting.

`NewNullBackend` creates a backend which swallows all errors and does not send them to Honeybadger. This is useful for development and testing to disable sending unnecessary errors.
Expand Down
46 changes: 43 additions & 3 deletions client.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package honeybadger

import (
"errors"
"net/http"
)

Expand All @@ -14,9 +15,13 @@ type Payload interface {
// custom implementation may be configured by the user.
type Backend interface {
Notify(feature Feature, payload Payload) error
Event(events []*eventPayload) error
}

var ErrEventDropped = errors.New("event dropped by handler")

type noticeHandler func(*Notice) error
type eventHandler func(map[string]any) error

// Client is the manager for interacting with the Honeybadger service. It holds
// the configuration and implements the public API.
Expand All @@ -25,11 +30,22 @@ type Client struct {
context *contextSync
worker worker
beforeNotifyHandlers []noticeHandler
eventsWorker *EventsWorker
beforeEventHandlers []eventHandler
}

func eventsConfigChanged(config *Configuration) bool {
return config.EventsBatchSize > 0 || config.EventsTimeout > 0 || config.EventsMaxQueueSize > 0 || config.EventsMaxRetries >= 0 || config.Backend != nil || config.Context != nil
}

// Configure updates the client configuration with the supplied config.
func (client *Client) Configure(config Configuration) {
client.Config.update(&config)

if eventsConfigChanged(&config) && client.eventsWorker != nil {
client.eventsWorker.Stop()
client.eventsWorker = NewEventsWorker(client.Config)
}
}

// SetContext updates the client context with supplied context.
Expand All @@ -40,6 +56,7 @@ func (client *Client) SetContext(context Context) {
// Flush blocks until the worker has processed its queue.
func (client *Client) Flush() {
client.worker.Flush()
client.eventsWorker.Flush()
}

// BeforeNotify adds a callback function which is run before a notice is
Expand All @@ -49,6 +66,10 @@ func (client *Client) BeforeNotify(handler func(notice *Notice) error) {
client.beforeNotifyHandlers = append(client.beforeNotifyHandlers, handler)
}

func (client *Client) BeforeEvent(handler func(event map[string]any) error) {
client.beforeEventHandlers = append(client.beforeEventHandlers, handler)
}

// Notify reports the error err to the Honeybadger service.
func (client *Client) Notify(err interface{}, extra ...interface{}) (string, error) {
extra = append([]interface{}{client.context.internal}, extra...)
Expand Down Expand Up @@ -78,6 +99,23 @@ func (client *Client) Notify(err interface{}, extra ...interface{}) (string, err
return notice.Token, nil
}

func (client *Client) Event(eventType string, eventData map[string]interface{}) error {
event := newEventPayload(eventType, eventData)

for _, handler := range client.beforeEventHandlers {
err := handler(event.data)

if err == ErrEventDropped {
return nil
} else if err != nil {
return err
}
}

client.eventsWorker.Push(event)
return nil
}

// Monitor automatically reports panics which occur in the function it's called
// from. Must be deferred.
func (client *Client) Monitor() {
Expand Down Expand Up @@ -110,11 +148,13 @@ func (client *Client) Handler(h http.Handler) http.Handler {
func New(c Configuration) *Client {
config := newConfig(c)
worker := newBufferedWorker(config)
eventsWorker := NewEventsWorker(config)

client := Client{
Config: config,
worker: worker,
context: newContextSync(),
Config: config,
worker: worker,
context: newContextSync(),
eventsWorker: eventsWorker,
}

return &client
Expand Down
4 changes: 4 additions & 0 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@ func (b *mockBackend) Notify(_ Feature, n Payload) error {
return nil
}

func (b *mockBackend) Event(events []*eventPayload) error {
return nil
}

func mockClient(c Configuration) (Client, *mockWorker, *mockBackend) {
worker := &mockWorker{}
backend := &mockBackend{}
Expand Down
65 changes: 48 additions & 17 deletions configuration.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package honeybadger

import (
"context"
"log"
"os"
"strconv"
Expand All @@ -15,15 +16,21 @@ type Logger interface {

// Configuration manages the configuration for the client.
type Configuration struct {
APIKey string
Root string
Env string
Hostname string
Endpoint string
Sync bool
Timeout time.Duration
Logger Logger
Backend Backend
APIKey string
Root string
Env string
Hostname string
Endpoint string
Sync bool
Timeout time.Duration
Logger Logger
Backend Backend
Context context.Context
EventsBatchSize int
EventsThrottleWait time.Duration
EventsTimeout time.Duration
EventsMaxQueueSize int
EventsMaxRetries int
}

func (c1 *Configuration) update(c2 *Configuration) *Configuration {
Expand Down Expand Up @@ -51,21 +58,45 @@ func (c1 *Configuration) update(c2 *Configuration) *Configuration {
if c2.Backend != nil {
c1.Backend = c2.Backend
}
if c2.Context != nil {
c1.Context = c2.Context
}
if c2.EventsBatchSize > 0 {
c1.EventsBatchSize = c2.EventsBatchSize
}
if c2.EventsTimeout > 0 {
c1.EventsTimeout = c2.EventsTimeout
}
if c2.EventsMaxQueueSize > 0 {
c1.EventsMaxQueueSize = c2.EventsMaxQueueSize
}
if c2.EventsMaxRetries > 0 {
c1.EventsMaxRetries = c2.EventsMaxRetries
}
if c2.EventsThrottleWait > 0 {
c1.EventsThrottleWait = c2.EventsThrottleWait
}

c1.Sync = c2.Sync
return c1
}

func newConfig(c Configuration) *Configuration {
config := &Configuration{
APIKey: getEnv("HONEYBADGER_API_KEY"),
Root: getPWD(),
Env: getEnv("HONEYBADGER_ENV"),
Hostname: getHostname(),
Endpoint: getEnv("HONEYBADGER_ENDPOINT", "https://api.honeybadger.io"),
Timeout: getTimeout(),
Logger: log.New(os.Stderr, "[honeybadger] ", log.Flags()),
Sync: getSync(),
APIKey: getEnv("HONEYBADGER_API_KEY"),
Root: getPWD(),
Env: getEnv("HONEYBADGER_ENV"),
Hostname: getHostname(),
Endpoint: getEnv("HONEYBADGER_ENDPOINT", "https://api.honeybadger.io"),
Timeout: getTimeout(),
Logger: log.New(os.Stderr, "[honeybadger] ", log.Flags()),
Sync: getSync(),
Context: context.Background(),
EventsThrottleWait: 60 * time.Second,
EventsBatchSize: 1000,
EventsTimeout: 30 * time.Second,
EventsMaxQueueSize: 100000,
EventsMaxRetries: 3,
}
config.update(&c)

Expand Down
4 changes: 4 additions & 0 deletions configuration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ func (l *TestBackend) Notify(f Feature, p Payload) (err error) {
return
}

func (l *TestBackend) Event(events []*eventPayload) (err error) {
return
}

func TestUpdateConfig(t *testing.T) {
config := &Configuration{}
logger := &TestLogger{}
Expand Down
27 changes: 27 additions & 0 deletions event.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package honeybadger

import (
"maps"
"time"
)

type eventPayload struct {
data map[string]any
}

func (e *eventPayload) toJSON() []byte {
h := hash(e.data)
return h.toJSON()
}

func newEventPayload(eventType string, eventData map[string]any) *eventPayload {
data := make(map[string]any)
maps.Copy(data, eventData)

data["event_type"] = eventType
if _, ok := data["ts"]; !ok {
data["ts"] = time.Now().UTC().Format(time.RFC3339)
}

return &eventPayload{data: data}
}
Loading
Loading