Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
54 changes: 54 additions & 0 deletions _examples/metrics/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package main

import (
"time"

"github.com/getsentry/sentry-go"
"github.com/getsentry/sentry-go/attribute"
)

func main() {
err := sentry.Init(sentry.ClientOptions{
Dsn: "",
EnabnleMetrics: true,
})
if err != nil {
panic(err)
}
defer sentry.Flush(2 * time.Second)

ctx := sentry.NewContext()
meter := sentry.NewMeter[int](ctx)
// Attaching permanent attributes on the meter
meter.SetAttributes(
attribute.String("version", "1.0.0"),
)

// Count metrics to measure occurrences of an event.
meter.Count("sent_emails", 1, sentry.MeterOptions{
Attributes: []sentry.Attribute{
attribute.String("email.provider", "sendgrid"),
attribute.Int("email.number_of_recipients", 3),
},
})

// Distribution metrics to measure the statistical distribution of a set of values.
// Useful for measuring things and keeping track of the patterns, e.g. file sizes, response times, etc.
meter.Distribution("file_upload_size", 3.14, sentry.MeterOptions{
Unit: "MB", // Unit is optional, but it's recommended!
Attributes: []sentry.Attribute{
attribute.String("file.type", "image/png"),
attribute.String("bucket.region", "us-west-2"),
attribute.String("bucket.name", "user-uploads"),
},
})

// Gauge metrics to measure a value at a specific point in time.
// Useful for measuring values that can go up and down, e.g. temperature, memory usage, etc.
meter.Gauge("active_chat_conversations", 7, sentry.MeterOptions{
Unit: "chat_rooms", // Unit is optional, but it's recommended!
Attributes: []sentry.Attribute{
attribute.String("region", "asia-northeast1"),
},
})
}
127 changes: 127 additions & 0 deletions batch_meter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package sentry

import (
"context"
"sync"
"time"
)

type BatchMeter struct {
client *Client
metricsCh chan Metric
flushCh chan chan struct{}
cancel context.CancelFunc
wg sync.WaitGroup
startOnce sync.Once
shutdownOnce sync.Once
}

func NewBatchMeter(client *Client) *BatchMeter {
return &BatchMeter{
client: client,
metricsCh: make(chan Metric, batchSize),
flushCh: make(chan chan struct{}),
}
}

func (m *BatchMeter) Start() {
m.startOnce.Do(func() {
ctx, cancel := context.WithCancel(context.Background())
m.cancel = cancel
m.wg.Add(1)
go m.run(ctx)
})
}

func (m *BatchMeter) Flush(timeout <-chan struct{}) {
done := make(chan struct{})
select {
case m.flushCh <- done:
select {
case <-done:
case <-timeout:
}
case <-timeout:
}
}

func (m *BatchMeter) Shutdown() {
m.shutdownOnce.Do(func() {
if m.cancel != nil {
m.cancel()
m.wg.Wait()
}
})
}

func (m *BatchMeter) run(ctx context.Context) { // nolint:dupl
defer m.wg.Done()
var metrics []Metric
timer := time.NewTimer(batchTimeout)
defer timer.Stop()

for {
select {
case metric := <-m.metricsCh:
metrics = append(metrics, metric)
if len(metrics) >= batchSize {
m.processEvent(metrics)
metrics = nil
if !timer.Stop() {
<-timer.C
}
timer.Reset(batchTimeout)
}
case <-timer.C:
if len(metrics) > 0 {
m.processEvent(metrics)
metrics = nil
}
timer.Reset(batchTimeout)
case done := <-m.flushCh:
flushDrain:
for {
select {
case metric := <-m.metricsCh:
metrics = append(metrics, metric)
default:
break flushDrain
}
}

if len(metrics) > 0 {
m.processEvent(metrics)
metrics = nil
}
if !timer.Stop() {
<-timer.C
}
timer.Reset(batchTimeout)
close(done)
case <-ctx.Done():
drain:
for {
select {
case metric := <-m.metricsCh:
metrics = append(metrics, metric)
default:
break drain
}
}

if len(metrics) > 0 {
m.processEvent(metrics)
}
return
}
}
}

func (m *BatchMeter) processEvent(metrics []Metric) {
event := NewEvent()
event.Timestamp = time.Now()
event.EventID = EventID(uuid())
event.Type = traceMetricEvent.Type
event.Metrics = metrics
m.client.Transport.SendEvent(event)
}
20 changes: 19 additions & 1 deletion client.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,9 @@ type ClientOptions struct {
BeforeSendTransaction func(event *Event, hint *EventHint) *Event
// Before breadcrumb add callback.
BeforeBreadcrumb func(breadcrumb *Breadcrumb, hint *BreadcrumbHint) *Breadcrumb
// BeforeSendMetric is called before metric events are sent to Sentry.
// You can use it to mutate the metric or return nil to discard it.
BeforeSendMetric func(metric *Metric) *Metric
// Integrations to be installed on the current Client, receives default
// integrations.
Integrations func([]Integration) []Integration
Expand Down Expand Up @@ -238,6 +241,8 @@ type ClientOptions struct {
Tags map[string]string
// EnableLogs controls when logs should be emitted.
EnableLogs bool
// EnableMetrics controls when metrics should be emitted.
EnableMetrics bool
// TraceIgnoreStatusCodes is a list of HTTP status codes that should not be traced.
// Each element can be either:
// - A single-element slice [code] for a specific status code
Expand Down Expand Up @@ -274,6 +279,7 @@ type Client struct {
// not supported, create a new client instead.
Transport Transport
batchLogger *BatchLogger
batchMeter *BatchMeter
telemetryBuffer *telemetry.Buffer
}

Expand Down Expand Up @@ -389,6 +395,11 @@ func NewClient(options ClientOptions) (*Client, error) {
client.batchLogger.Start()
}

if options.EnableMetrics {
client.batchMeter = NewBatchMeter(&client)
client.batchMeter.Start()
}

client.setupIntegrations()

return &client, nil
Expand Down Expand Up @@ -446,6 +457,7 @@ func (client *Client) setupTelemetryBuffer() { // nolint: unused
ratelimit.CategoryTransaction: telemetry.NewRingBuffer[protocol.EnvelopeItemConvertible](ratelimit.CategoryTransaction, 1000, telemetry.OverflowPolicyDropOldest, 1, 0),
ratelimit.CategoryLog: telemetry.NewRingBuffer[protocol.EnvelopeItemConvertible](ratelimit.CategoryLog, 10*100, telemetry.OverflowPolicyDropOldest, 100, 5*time.Second),
ratelimit.CategoryMonitor: telemetry.NewRingBuffer[protocol.EnvelopeItemConvertible](ratelimit.CategoryMonitor, 100, telemetry.OverflowPolicyDropOldest, 1, 0),
ratelimit.CategoryTraceMetric: telemetry.NewRingBuffer[protocol.EnvelopeItemConvertible](ratelimit.CategoryTraceMetric, 1000, telemetry.OverflowPolicyDropOldest, 1, 0),
}

sdkInfo := &protocol.SdkInfo{
Expand Down Expand Up @@ -596,7 +608,7 @@ func (client *Client) RecoverWithContext(
// the network synchronously, configure it to use the HTTPSyncTransport in the
// call to Init.
func (client *Client) Flush(timeout time.Duration) bool {
if client.batchLogger != nil || client.telemetryBuffer != nil {
if client.batchLogger != nil || client.batchMeter != nil || client.telemetryBuffer != nil {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
return client.FlushWithContext(ctx)
Expand All @@ -620,6 +632,9 @@ func (client *Client) FlushWithContext(ctx context.Context) bool {
if client.batchLogger != nil {
client.batchLogger.Flush(ctx.Done())
}
if client.batchMeter != nil {
client.batchMeter.Flush(ctx.Done())
}
if client.telemetryBuffer != nil {
return client.telemetryBuffer.FlushWithContext(ctx)
}
Expand All @@ -637,6 +652,9 @@ func (client *Client) Close() {
if client.batchLogger != nil {
client.batchLogger.Shutdown()
}
if client.batchMeter != nil {
client.batchMeter.Shutdown()
}
client.Transport.Close()
}

Expand Down
Loading
Loading