Update dependencies
This commit is contained in:
35
vendor/github.com/getsentry/sentry-go/internal/debuglog/log.go
generated
vendored
Normal file
35
vendor/github.com/getsentry/sentry-go/internal/debuglog/log.go
generated
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
package debuglog
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
)
|
||||
|
||||
// logger is the global debug logger instance.
|
||||
var logger = log.New(io.Discard, "[Sentry] ", log.LstdFlags)
|
||||
|
||||
// SetOutput changes the output destination of the logger.
|
||||
func SetOutput(w io.Writer) {
|
||||
logger.SetOutput(w)
|
||||
}
|
||||
|
||||
// GetLogger returns the current logger instance.
|
||||
// This function is thread-safe and can be called concurrently.
|
||||
func GetLogger() *log.Logger {
|
||||
return logger
|
||||
}
|
||||
|
||||
// Printf calls Printf on the underlying logger.
|
||||
func Printf(format string, args ...interface{}) {
|
||||
logger.Printf(format, args...)
|
||||
}
|
||||
|
||||
// Println calls Println on the underlying logger.
|
||||
func Println(args ...interface{}) {
|
||||
logger.Println(args...)
|
||||
}
|
||||
|
||||
// Print calls Print on the underlying logger.
|
||||
func Print(args ...interface{}) {
|
||||
logger.Print(args...)
|
||||
}
|
||||
572
vendor/github.com/getsentry/sentry-go/internal/http/transport.go
generated
vendored
Normal file
572
vendor/github.com/getsentry/sentry-go/internal/http/transport.go
generated
vendored
Normal file
@@ -0,0 +1,572 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/getsentry/sentry-go/internal/debuglog"
|
||||
"github.com/getsentry/sentry-go/internal/protocol"
|
||||
"github.com/getsentry/sentry-go/internal/ratelimit"
|
||||
)
|
||||
|
||||
const (
|
||||
apiVersion = 7
|
||||
|
||||
defaultTimeout = time.Second * 30
|
||||
defaultQueueSize = 1000
|
||||
|
||||
// maxDrainResponseBytes is the maximum number of bytes that transport
|
||||
// implementations will read from response bodies when draining them.
|
||||
maxDrainResponseBytes = 16 << 10
|
||||
)
|
||||
|
||||
var (
|
||||
ErrTransportQueueFull = errors.New("transport queue full")
|
||||
ErrTransportClosed = errors.New("transport is closed")
|
||||
ErrEmptyEnvelope = errors.New("empty envelope provided")
|
||||
)
|
||||
|
||||
type TransportOptions struct {
|
||||
Dsn string
|
||||
HTTPClient *http.Client
|
||||
HTTPTransport http.RoundTripper
|
||||
HTTPProxy string
|
||||
HTTPSProxy string
|
||||
CaCerts *x509.CertPool
|
||||
}
|
||||
|
||||
func getProxyConfig(options TransportOptions) func(*http.Request) (*url.URL, error) {
|
||||
if len(options.HTTPSProxy) > 0 {
|
||||
return func(*http.Request) (*url.URL, error) {
|
||||
return url.Parse(options.HTTPSProxy)
|
||||
}
|
||||
}
|
||||
|
||||
if len(options.HTTPProxy) > 0 {
|
||||
return func(*http.Request) (*url.URL, error) {
|
||||
return url.Parse(options.HTTPProxy)
|
||||
}
|
||||
}
|
||||
|
||||
return http.ProxyFromEnvironment
|
||||
}
|
||||
|
||||
func getTLSConfig(options TransportOptions) *tls.Config {
|
||||
if options.CaCerts != nil {
|
||||
return &tls.Config{
|
||||
RootCAs: options.CaCerts,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getSentryRequestFromEnvelope(ctx context.Context, dsn *protocol.Dsn, envelope *protocol.Envelope) (r *http.Request, err error) {
|
||||
defer func() {
|
||||
if r != nil {
|
||||
sdkName := envelope.Header.Sdk.Name
|
||||
sdkVersion := envelope.Header.Sdk.Version
|
||||
|
||||
r.Header.Set("User-Agent", fmt.Sprintf("%s/%s", sdkName, sdkVersion))
|
||||
r.Header.Set("Content-Type", "application/x-sentry-envelope")
|
||||
|
||||
auth := fmt.Sprintf("Sentry sentry_version=%d, "+
|
||||
"sentry_client=%s/%s, sentry_key=%s", apiVersion, sdkName, sdkVersion, dsn.GetPublicKey())
|
||||
|
||||
if dsn.GetSecretKey() != "" {
|
||||
auth = fmt.Sprintf("%s, sentry_secret=%s", auth, dsn.GetSecretKey())
|
||||
}
|
||||
|
||||
r.Header.Set("X-Sentry-Auth", auth)
|
||||
}
|
||||
}()
|
||||
|
||||
var buf bytes.Buffer
|
||||
_, err = envelope.WriteTo(&buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return http.NewRequestWithContext(
|
||||
ctx,
|
||||
http.MethodPost,
|
||||
dsn.GetAPIURL().String(),
|
||||
&buf,
|
||||
)
|
||||
}
|
||||
|
||||
func categoryFromEnvelope(envelope *protocol.Envelope) ratelimit.Category {
|
||||
if envelope == nil || len(envelope.Items) == 0 {
|
||||
return ratelimit.CategoryAll
|
||||
}
|
||||
|
||||
for _, item := range envelope.Items {
|
||||
if item == nil || item.Header == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
switch item.Header.Type {
|
||||
case protocol.EnvelopeItemTypeEvent:
|
||||
return ratelimit.CategoryError
|
||||
case protocol.EnvelopeItemTypeTransaction:
|
||||
return ratelimit.CategoryTransaction
|
||||
case protocol.EnvelopeItemTypeCheckIn:
|
||||
return ratelimit.CategoryMonitor
|
||||
case protocol.EnvelopeItemTypeLog:
|
||||
return ratelimit.CategoryLog
|
||||
case protocol.EnvelopeItemTypeAttachment:
|
||||
continue
|
||||
default:
|
||||
return ratelimit.CategoryAll
|
||||
}
|
||||
}
|
||||
|
||||
return ratelimit.CategoryAll
|
||||
}
|
||||
|
||||
// SyncTransport is a blocking implementation of Transport.
|
||||
//
|
||||
// Clients using this transport will send requests to Sentry sequentially and
|
||||
// block until a response is returned.
|
||||
//
|
||||
// The blocking behavior is useful in a limited set of use cases. For example,
|
||||
// use it when deploying code to a Function as a Service ("Serverless")
|
||||
// platform, where any work happening in a background goroutine is not
|
||||
// guaranteed to execute.
|
||||
//
|
||||
// For most cases, prefer AsyncTransport.
|
||||
type SyncTransport struct {
|
||||
dsn *protocol.Dsn
|
||||
client *http.Client
|
||||
transport http.RoundTripper
|
||||
|
||||
mu sync.Mutex
|
||||
limits ratelimit.Map
|
||||
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
func NewSyncTransport(options TransportOptions) protocol.TelemetryTransport {
|
||||
dsn, err := protocol.NewDsn(options.Dsn)
|
||||
if err != nil || dsn == nil {
|
||||
debuglog.Printf("Transport is disabled: invalid dsn: %v\n", err)
|
||||
return NewNoopTransport()
|
||||
}
|
||||
|
||||
transport := &SyncTransport{
|
||||
Timeout: defaultTimeout,
|
||||
limits: make(ratelimit.Map),
|
||||
dsn: dsn,
|
||||
}
|
||||
|
||||
if options.HTTPTransport != nil {
|
||||
transport.transport = options.HTTPTransport
|
||||
} else {
|
||||
transport.transport = &http.Transport{
|
||||
Proxy: getProxyConfig(options),
|
||||
TLSClientConfig: getTLSConfig(options),
|
||||
}
|
||||
}
|
||||
|
||||
if options.HTTPClient != nil {
|
||||
transport.client = options.HTTPClient
|
||||
} else {
|
||||
transport.client = &http.Client{
|
||||
Transport: transport.transport,
|
||||
Timeout: transport.Timeout,
|
||||
}
|
||||
}
|
||||
|
||||
return transport
|
||||
}
|
||||
|
||||
func (t *SyncTransport) SendEnvelope(envelope *protocol.Envelope) error {
|
||||
return t.SendEnvelopeWithContext(context.Background(), envelope)
|
||||
}
|
||||
|
||||
func (t *SyncTransport) Close() {}
|
||||
|
||||
func (t *SyncTransport) IsRateLimited(category ratelimit.Category) bool {
|
||||
return t.disabled(category)
|
||||
}
|
||||
|
||||
func (t *SyncTransport) HasCapacity() bool { return true }
|
||||
|
||||
func (t *SyncTransport) SendEnvelopeWithContext(ctx context.Context, envelope *protocol.Envelope) error {
|
||||
if envelope == nil || len(envelope.Items) == 0 {
|
||||
return ErrEmptyEnvelope
|
||||
}
|
||||
|
||||
category := categoryFromEnvelope(envelope)
|
||||
if t.disabled(category) {
|
||||
return nil
|
||||
}
|
||||
|
||||
request, err := getSentryRequestFromEnvelope(ctx, t.dsn, envelope)
|
||||
if err != nil {
|
||||
debuglog.Printf("There was an issue creating the request: %v", err)
|
||||
return err
|
||||
}
|
||||
debuglog.Printf(
|
||||
"Sending %s [%s] to %s project: %s",
|
||||
envelope.Items[0].Header.Type,
|
||||
envelope.Header.EventID,
|
||||
t.dsn.GetHost(),
|
||||
t.dsn.GetProjectID(),
|
||||
)
|
||||
|
||||
response, err := t.client.Do(request)
|
||||
if err != nil {
|
||||
debuglog.Printf("There was an issue with sending an event: %v", err)
|
||||
return err
|
||||
}
|
||||
if response.StatusCode >= 400 && response.StatusCode <= 599 {
|
||||
b, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
debuglog.Printf("Error while reading response body: %v", err)
|
||||
}
|
||||
debuglog.Printf("Sending %s failed with the following error: %s", envelope.Header.EventID, string(b))
|
||||
}
|
||||
|
||||
t.mu.Lock()
|
||||
if t.limits == nil {
|
||||
t.limits = make(ratelimit.Map)
|
||||
}
|
||||
|
||||
t.limits.Merge(ratelimit.FromResponse(response))
|
||||
t.mu.Unlock()
|
||||
|
||||
_, _ = io.CopyN(io.Discard, response.Body, maxDrainResponseBytes)
|
||||
return response.Body.Close()
|
||||
}
|
||||
|
||||
func (t *SyncTransport) Flush(_ time.Duration) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (t *SyncTransport) FlushWithContext(_ context.Context) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (t *SyncTransport) disabled(c ratelimit.Category) bool {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
disabled := t.limits.IsRateLimited(c)
|
||||
if disabled {
|
||||
debuglog.Printf("Too many requests for %q, backing off till: %v", c, t.limits.Deadline(c))
|
||||
}
|
||||
return disabled
|
||||
}
|
||||
|
||||
// AsyncTransport is the default, non-blocking, implementation of Transport.
|
||||
//
|
||||
// Clients using this transport will enqueue requests in a queue and return to
|
||||
// the caller before any network communication has happened. Requests are sent
|
||||
// to Sentry sequentially from a background goroutine.
|
||||
type AsyncTransport struct {
|
||||
dsn *protocol.Dsn
|
||||
client *http.Client
|
||||
transport http.RoundTripper
|
||||
|
||||
queue chan *protocol.Envelope
|
||||
|
||||
mu sync.RWMutex
|
||||
limits ratelimit.Map
|
||||
|
||||
done chan struct{}
|
||||
wg sync.WaitGroup
|
||||
|
||||
flushRequest chan chan struct{}
|
||||
|
||||
sentCount int64
|
||||
droppedCount int64
|
||||
errorCount int64
|
||||
|
||||
QueueSize int
|
||||
Timeout time.Duration
|
||||
|
||||
startOnce sync.Once
|
||||
closeOnce sync.Once
|
||||
}
|
||||
|
||||
func NewAsyncTransport(options TransportOptions) protocol.TelemetryTransport {
|
||||
dsn, err := protocol.NewDsn(options.Dsn)
|
||||
if err != nil || dsn == nil {
|
||||
debuglog.Printf("Transport is disabled: invalid dsn: %v", err)
|
||||
return NewNoopTransport()
|
||||
}
|
||||
|
||||
transport := &AsyncTransport{
|
||||
QueueSize: defaultQueueSize,
|
||||
Timeout: defaultTimeout,
|
||||
done: make(chan struct{}),
|
||||
limits: make(ratelimit.Map),
|
||||
dsn: dsn,
|
||||
}
|
||||
|
||||
transport.queue = make(chan *protocol.Envelope, transport.QueueSize)
|
||||
transport.flushRequest = make(chan chan struct{})
|
||||
|
||||
if options.HTTPTransport != nil {
|
||||
transport.transport = options.HTTPTransport
|
||||
} else {
|
||||
transport.transport = &http.Transport{
|
||||
Proxy: getProxyConfig(options),
|
||||
TLSClientConfig: getTLSConfig(options),
|
||||
}
|
||||
}
|
||||
|
||||
if options.HTTPClient != nil {
|
||||
transport.client = options.HTTPClient
|
||||
} else {
|
||||
transport.client = &http.Client{
|
||||
Transport: transport.transport,
|
||||
Timeout: transport.Timeout,
|
||||
}
|
||||
}
|
||||
|
||||
transport.start()
|
||||
return transport
|
||||
}
|
||||
|
||||
func (t *AsyncTransport) start() {
|
||||
t.startOnce.Do(func() {
|
||||
t.wg.Add(1)
|
||||
go t.worker()
|
||||
})
|
||||
}
|
||||
|
||||
// HasCapacity reports whether the async transport queue appears to have space
|
||||
// for at least one more envelope. This is a best-effort, non-blocking check.
|
||||
func (t *AsyncTransport) HasCapacity() bool {
|
||||
t.mu.RLock()
|
||||
defer t.mu.RUnlock()
|
||||
select {
|
||||
case <-t.done:
|
||||
return false
|
||||
default:
|
||||
}
|
||||
return len(t.queue) < cap(t.queue)
|
||||
}
|
||||
|
||||
func (t *AsyncTransport) SendEnvelope(envelope *protocol.Envelope) error {
|
||||
select {
|
||||
case <-t.done:
|
||||
return ErrTransportClosed
|
||||
default:
|
||||
}
|
||||
|
||||
if envelope == nil || len(envelope.Items) == 0 {
|
||||
return ErrEmptyEnvelope
|
||||
}
|
||||
|
||||
category := categoryFromEnvelope(envelope)
|
||||
if t.isRateLimited(category) {
|
||||
return nil
|
||||
}
|
||||
|
||||
select {
|
||||
case t.queue <- envelope:
|
||||
debuglog.Printf(
|
||||
"Sending %s [%s] to %s project: %s",
|
||||
envelope.Items[0].Header.Type,
|
||||
envelope.Header.EventID,
|
||||
t.dsn.GetHost(),
|
||||
t.dsn.GetProjectID(),
|
||||
)
|
||||
return nil
|
||||
default:
|
||||
atomic.AddInt64(&t.droppedCount, 1)
|
||||
return ErrTransportQueueFull
|
||||
}
|
||||
}
|
||||
|
||||
func (t *AsyncTransport) Flush(timeout time.Duration) bool {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
return t.FlushWithContext(ctx)
|
||||
}
|
||||
|
||||
func (t *AsyncTransport) FlushWithContext(ctx context.Context) bool {
|
||||
flushResponse := make(chan struct{})
|
||||
select {
|
||||
case t.flushRequest <- flushResponse:
|
||||
select {
|
||||
case <-flushResponse:
|
||||
debuglog.Println("Buffer flushed successfully.")
|
||||
return true
|
||||
case <-ctx.Done():
|
||||
debuglog.Println("Failed to flush, buffer timed out.")
|
||||
return false
|
||||
}
|
||||
case <-ctx.Done():
|
||||
debuglog.Println("Failed to flush, buffer timed out.")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (t *AsyncTransport) Close() {
|
||||
t.closeOnce.Do(func() {
|
||||
close(t.done)
|
||||
close(t.queue)
|
||||
close(t.flushRequest)
|
||||
t.wg.Wait()
|
||||
})
|
||||
}
|
||||
|
||||
func (t *AsyncTransport) IsRateLimited(category ratelimit.Category) bool {
|
||||
return t.isRateLimited(category)
|
||||
}
|
||||
|
||||
func (t *AsyncTransport) worker() {
|
||||
defer t.wg.Done()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-t.done:
|
||||
return
|
||||
case envelope, open := <-t.queue:
|
||||
if !open {
|
||||
return
|
||||
}
|
||||
t.processEnvelope(envelope)
|
||||
case flushResponse, open := <-t.flushRequest:
|
||||
if !open {
|
||||
return
|
||||
}
|
||||
t.drainQueue()
|
||||
close(flushResponse)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *AsyncTransport) drainQueue() {
|
||||
for {
|
||||
select {
|
||||
case envelope, open := <-t.queue:
|
||||
if !open {
|
||||
return
|
||||
}
|
||||
t.processEnvelope(envelope)
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *AsyncTransport) processEnvelope(envelope *protocol.Envelope) {
|
||||
if t.sendEnvelopeHTTP(envelope) {
|
||||
atomic.AddInt64(&t.sentCount, 1)
|
||||
} else {
|
||||
atomic.AddInt64(&t.errorCount, 1)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *AsyncTransport) sendEnvelopeHTTP(envelope *protocol.Envelope) bool {
|
||||
category := categoryFromEnvelope(envelope)
|
||||
if t.isRateLimited(category) {
|
||||
return false
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
|
||||
defer cancel()
|
||||
|
||||
request, err := getSentryRequestFromEnvelope(ctx, t.dsn, envelope)
|
||||
if err != nil {
|
||||
debuglog.Printf("Failed to create request from envelope: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
response, err := t.client.Do(request)
|
||||
if err != nil {
|
||||
debuglog.Printf("HTTP request failed: %v", err)
|
||||
return false
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
success := t.handleResponse(response)
|
||||
|
||||
t.mu.Lock()
|
||||
if t.limits == nil {
|
||||
t.limits = make(ratelimit.Map)
|
||||
}
|
||||
t.limits.Merge(ratelimit.FromResponse(response))
|
||||
t.mu.Unlock()
|
||||
|
||||
_, _ = io.CopyN(io.Discard, response.Body, maxDrainResponseBytes)
|
||||
return success
|
||||
}
|
||||
|
||||
func (t *AsyncTransport) handleResponse(response *http.Response) bool {
|
||||
if response.StatusCode >= 200 && response.StatusCode < 300 {
|
||||
return true
|
||||
}
|
||||
|
||||
if response.StatusCode >= 400 && response.StatusCode < 500 {
|
||||
if body, err := io.ReadAll(io.LimitReader(response.Body, maxDrainResponseBytes)); err == nil {
|
||||
debuglog.Printf("Client error %d: %s", response.StatusCode, string(body))
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if response.StatusCode >= 500 {
|
||||
debuglog.Printf("Server error %d", response.StatusCode)
|
||||
return false
|
||||
}
|
||||
|
||||
debuglog.Printf("Unexpected status code %d", response.StatusCode)
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *AsyncTransport) isRateLimited(category ratelimit.Category) bool {
|
||||
t.mu.RLock()
|
||||
defer t.mu.RUnlock()
|
||||
limited := t.limits.IsRateLimited(category)
|
||||
if limited {
|
||||
debuglog.Printf("Rate limited for category %q until %v", category, t.limits.Deadline(category))
|
||||
}
|
||||
return limited
|
||||
}
|
||||
|
||||
// NoopTransport is a transport implementation that drops all events.
|
||||
// Used internally when an empty or invalid DSN is provided.
|
||||
type NoopTransport struct{}
|
||||
|
||||
func NewNoopTransport() *NoopTransport {
|
||||
debuglog.Println("Transport initialized with invalid DSN. Using NoopTransport. No events will be delivered.")
|
||||
return &NoopTransport{}
|
||||
}
|
||||
|
||||
func (t *NoopTransport) SendEnvelope(_ *protocol.Envelope) error {
|
||||
debuglog.Println("Envelope dropped due to NoopTransport usage.")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *NoopTransport) IsRateLimited(_ ratelimit.Category) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *NoopTransport) Flush(_ time.Duration) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (t *NoopTransport) FlushWithContext(_ context.Context) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (t *NoopTransport) Close() {
|
||||
// Nothing to close
|
||||
}
|
||||
|
||||
func (t *NoopTransport) HasCapacity() bool { return true }
|
||||
236
vendor/github.com/getsentry/sentry-go/internal/protocol/dsn.go
generated
vendored
Normal file
236
vendor/github.com/getsentry/sentry-go/internal/protocol/dsn.go
generated
vendored
Normal file
@@ -0,0 +1,236 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// apiVersion is the version of the Sentry API.
|
||||
const apiVersion = "7"
|
||||
|
||||
type scheme string
|
||||
|
||||
const (
|
||||
SchemeHTTP scheme = "http"
|
||||
SchemeHTTPS scheme = "https"
|
||||
)
|
||||
|
||||
func (scheme scheme) defaultPort() int {
|
||||
switch scheme {
|
||||
case SchemeHTTPS:
|
||||
return 443
|
||||
case SchemeHTTP:
|
||||
return 80
|
||||
default:
|
||||
return 80
|
||||
}
|
||||
}
|
||||
|
||||
// DsnParseError represents an error that occurs if a Sentry
|
||||
// DSN cannot be parsed.
|
||||
type DsnParseError struct {
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e DsnParseError) Error() string {
|
||||
return "[Sentry] DsnParseError: " + e.Message
|
||||
}
|
||||
|
||||
// Dsn is used as the remote address source to client transport.
|
||||
type Dsn struct {
|
||||
scheme scheme
|
||||
publicKey string
|
||||
secretKey string
|
||||
host string
|
||||
port int
|
||||
path string
|
||||
projectID string
|
||||
}
|
||||
|
||||
// NewDsn creates a Dsn by parsing rawURL. Most users will never call this
|
||||
// function directly. It is provided for use in custom Transport
|
||||
// implementations.
|
||||
func NewDsn(rawURL string) (*Dsn, error) {
|
||||
// Parse
|
||||
parsedURL, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return nil, &DsnParseError{fmt.Sprintf("invalid url: %v", err)}
|
||||
}
|
||||
|
||||
// Scheme
|
||||
var scheme scheme
|
||||
switch parsedURL.Scheme {
|
||||
case "http":
|
||||
scheme = SchemeHTTP
|
||||
case "https":
|
||||
scheme = SchemeHTTPS
|
||||
default:
|
||||
return nil, &DsnParseError{"invalid scheme"}
|
||||
}
|
||||
|
||||
// PublicKey
|
||||
publicKey := parsedURL.User.Username()
|
||||
if publicKey == "" {
|
||||
return nil, &DsnParseError{"empty username"}
|
||||
}
|
||||
|
||||
// SecretKey
|
||||
var secretKey string
|
||||
if parsedSecretKey, ok := parsedURL.User.Password(); ok {
|
||||
secretKey = parsedSecretKey
|
||||
}
|
||||
|
||||
// Host
|
||||
host := parsedURL.Hostname()
|
||||
if host == "" {
|
||||
return nil, &DsnParseError{"empty host"}
|
||||
}
|
||||
|
||||
// Port
|
||||
var port int
|
||||
if p := parsedURL.Port(); p != "" {
|
||||
port, err = strconv.Atoi(p)
|
||||
if err != nil {
|
||||
return nil, &DsnParseError{"invalid port"}
|
||||
}
|
||||
} else {
|
||||
port = scheme.defaultPort()
|
||||
}
|
||||
|
||||
// ProjectID
|
||||
if parsedURL.Path == "" || parsedURL.Path == "/" {
|
||||
return nil, &DsnParseError{"empty project id"}
|
||||
}
|
||||
pathSegments := strings.Split(parsedURL.Path[1:], "/")
|
||||
projectID := pathSegments[len(pathSegments)-1]
|
||||
|
||||
if projectID == "" {
|
||||
return nil, &DsnParseError{"empty project id"}
|
||||
}
|
||||
|
||||
// Path
|
||||
var path string
|
||||
if len(pathSegments) > 1 {
|
||||
path = "/" + strings.Join(pathSegments[0:len(pathSegments)-1], "/")
|
||||
}
|
||||
|
||||
return &Dsn{
|
||||
scheme: scheme,
|
||||
publicKey: publicKey,
|
||||
secretKey: secretKey,
|
||||
host: host,
|
||||
port: port,
|
||||
path: path,
|
||||
projectID: projectID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// String formats Dsn struct into a valid string url.
|
||||
func (dsn Dsn) String() string {
|
||||
var url string
|
||||
url += fmt.Sprintf("%s://%s", dsn.scheme, dsn.publicKey)
|
||||
if dsn.secretKey != "" {
|
||||
url += fmt.Sprintf(":%s", dsn.secretKey)
|
||||
}
|
||||
url += fmt.Sprintf("@%s", dsn.host)
|
||||
if dsn.port != dsn.scheme.defaultPort() {
|
||||
url += fmt.Sprintf(":%d", dsn.port)
|
||||
}
|
||||
if dsn.path != "" {
|
||||
url += dsn.path
|
||||
}
|
||||
url += fmt.Sprintf("/%s", dsn.projectID)
|
||||
return url
|
||||
}
|
||||
|
||||
// Get the scheme of the DSN.
|
||||
func (dsn Dsn) GetScheme() string {
|
||||
return string(dsn.scheme)
|
||||
}
|
||||
|
||||
// Get the public key of the DSN.
|
||||
func (dsn Dsn) GetPublicKey() string {
|
||||
return dsn.publicKey
|
||||
}
|
||||
|
||||
// Get the secret key of the DSN.
|
||||
func (dsn Dsn) GetSecretKey() string {
|
||||
return dsn.secretKey
|
||||
}
|
||||
|
||||
// Get the host of the DSN.
|
||||
func (dsn Dsn) GetHost() string {
|
||||
return dsn.host
|
||||
}
|
||||
|
||||
// Get the port of the DSN.
|
||||
func (dsn Dsn) GetPort() int {
|
||||
return dsn.port
|
||||
}
|
||||
|
||||
// Get the path of the DSN.
|
||||
func (dsn Dsn) GetPath() string {
|
||||
return dsn.path
|
||||
}
|
||||
|
||||
// Get the project ID of the DSN.
|
||||
func (dsn Dsn) GetProjectID() string {
|
||||
return dsn.projectID
|
||||
}
|
||||
|
||||
// GetAPIURL returns the URL of the envelope endpoint of the project
|
||||
// associated with the DSN.
|
||||
func (dsn Dsn) GetAPIURL() *url.URL {
|
||||
var rawURL string
|
||||
rawURL += fmt.Sprintf("%s://%s", dsn.scheme, dsn.host)
|
||||
if dsn.port != dsn.scheme.defaultPort() {
|
||||
rawURL += fmt.Sprintf(":%d", dsn.port)
|
||||
}
|
||||
if dsn.path != "" {
|
||||
rawURL += dsn.path
|
||||
}
|
||||
rawURL += fmt.Sprintf("/api/%s/%s/", dsn.projectID, "envelope")
|
||||
parsedURL, _ := url.Parse(rawURL)
|
||||
return parsedURL
|
||||
}
|
||||
|
||||
// RequestHeaders returns all the necessary headers that have to be used in the transport when sending events
|
||||
// to the /store endpoint.
|
||||
//
|
||||
// Deprecated: This method shall only be used if you want to implement your own transport that sends events to
|
||||
// the /store endpoint. If you're using the transport provided by the SDK, all necessary headers to authenticate
|
||||
// against the /envelope endpoint are added automatically.
|
||||
func (dsn Dsn) RequestHeaders(sdkVersion string) map[string]string {
|
||||
auth := fmt.Sprintf("Sentry sentry_version=%s, sentry_timestamp=%d, "+
|
||||
"sentry_client=sentry.go/%s, sentry_key=%s", apiVersion, time.Now().Unix(), sdkVersion, dsn.publicKey)
|
||||
|
||||
if dsn.secretKey != "" {
|
||||
auth = fmt.Sprintf("%s, sentry_secret=%s", auth, dsn.secretKey)
|
||||
}
|
||||
|
||||
return map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
"X-Sentry-Auth": auth,
|
||||
}
|
||||
}
|
||||
|
||||
// MarshalJSON converts the Dsn struct to JSON.
|
||||
func (dsn Dsn) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(dsn.String())
|
||||
}
|
||||
|
||||
// UnmarshalJSON converts JSON data to the Dsn struct.
|
||||
func (dsn *Dsn) UnmarshalJSON(data []byte) error {
|
||||
var str string
|
||||
_ = json.Unmarshal(data, &str)
|
||||
newDsn, err := NewDsn(str)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*dsn = *newDsn
|
||||
return nil
|
||||
}
|
||||
231
vendor/github.com/getsentry/sentry-go/internal/protocol/envelope.go
generated
vendored
Normal file
231
vendor/github.com/getsentry/sentry-go/internal/protocol/envelope.go
generated
vendored
Normal file
@@ -0,0 +1,231 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Envelope represents a Sentry envelope containing headers and items.
|
||||
type Envelope struct {
|
||||
Header *EnvelopeHeader `json:"-"`
|
||||
Items []*EnvelopeItem `json:"-"`
|
||||
}
|
||||
|
||||
// EnvelopeHeader represents the header of a Sentry envelope.
|
||||
type EnvelopeHeader struct {
|
||||
// EventID is the unique identifier for this event
|
||||
EventID string `json:"event_id"`
|
||||
|
||||
// SentAt is the timestamp when the event was sent from the SDK as string in RFC 3339 format.
|
||||
// Used for clock drift correction of the event timestamp. The time zone must be UTC.
|
||||
SentAt time.Time `json:"sent_at,omitempty"`
|
||||
|
||||
// Dsn can be used for self-authenticated envelopes.
|
||||
// This means that the envelope has all the information necessary to be sent to sentry.
|
||||
// In this case the full DSN must be stored in this key.
|
||||
Dsn string `json:"dsn,omitempty"`
|
||||
|
||||
// Sdk carries the same payload as the sdk interface in the event payload but can be carried for all events.
|
||||
// This means that SDK information can be carried for minidumps, session data and other submissions.
|
||||
Sdk *SdkInfo `json:"sdk,omitempty"`
|
||||
|
||||
// Trace contains the [Dynamic Sampling Context](https://develop.sentry.dev/sdk/telemetry/traces/dynamic-sampling-context/)
|
||||
Trace map[string]string `json:"trace,omitempty"`
|
||||
}
|
||||
|
||||
// EnvelopeItemType represents the type of envelope item.
|
||||
type EnvelopeItemType string
|
||||
|
||||
// Constants for envelope item types as defined in the Sentry documentation.
|
||||
const (
|
||||
EnvelopeItemTypeEvent EnvelopeItemType = "event"
|
||||
EnvelopeItemTypeTransaction EnvelopeItemType = "transaction"
|
||||
EnvelopeItemTypeCheckIn EnvelopeItemType = "check_in"
|
||||
EnvelopeItemTypeAttachment EnvelopeItemType = "attachment"
|
||||
EnvelopeItemTypeLog EnvelopeItemType = "log"
|
||||
EnvelopeItemTypeTraceMetric EnvelopeItemType = "trace_metric"
|
||||
)
|
||||
|
||||
// EnvelopeItemHeader represents the header of an envelope item.
|
||||
type EnvelopeItemHeader struct {
|
||||
// Type specifies the type of this Item and its contents.
|
||||
// Based on the Item type, more headers may be required.
|
||||
Type EnvelopeItemType `json:"type"`
|
||||
|
||||
// Length is the length of the payload in bytes.
|
||||
// If no length is specified, the payload implicitly goes to the next newline.
|
||||
// For payloads containing newline characters, the length must be specified.
|
||||
Length *int `json:"length,omitempty"`
|
||||
|
||||
// Filename is the name of the attachment file (used for attachments)
|
||||
Filename string `json:"filename,omitempty"`
|
||||
|
||||
// ContentType is the MIME type of the item payload (used for attachments and some other item types)
|
||||
ContentType string `json:"content_type,omitempty"`
|
||||
|
||||
// ItemCount is the number of items in a batch (used for logs)
|
||||
ItemCount *int `json:"item_count,omitempty"`
|
||||
}
|
||||
|
||||
// EnvelopeItem represents a single item within an envelope.
|
||||
type EnvelopeItem struct {
|
||||
Header *EnvelopeItemHeader `json:"-"`
|
||||
Payload []byte `json:"-"`
|
||||
}
|
||||
|
||||
// NewEnvelope creates a new envelope with the given header.
|
||||
func NewEnvelope(header *EnvelopeHeader) *Envelope {
|
||||
return &Envelope{
|
||||
Header: header,
|
||||
Items: make([]*EnvelopeItem, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// AddItem adds an item to the envelope.
|
||||
func (e *Envelope) AddItem(item *EnvelopeItem) {
|
||||
if item == nil {
|
||||
return
|
||||
}
|
||||
e.Items = append(e.Items, item)
|
||||
}
|
||||
|
||||
// Serialize serializes the envelope to the Sentry envelope format.
|
||||
//
|
||||
// Format: Headers "\n" { Item } [ "\n" ]
|
||||
// Item: Headers "\n" Payload "\n".
|
||||
func (e *Envelope) Serialize() ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
|
||||
headerBytes, err := json.Marshal(e.Header)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal envelope header: %w", err)
|
||||
}
|
||||
|
||||
if _, err := buf.Write(headerBytes); err != nil {
|
||||
return nil, fmt.Errorf("failed to write envelope header: %w", err)
|
||||
}
|
||||
|
||||
if _, err := buf.WriteString("\n"); err != nil {
|
||||
return nil, fmt.Errorf("failed to write newline after envelope header: %w", err)
|
||||
}
|
||||
|
||||
for _, item := range e.Items {
|
||||
if err := e.writeItem(&buf, item); err != nil {
|
||||
return nil, fmt.Errorf("failed to write envelope item: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// WriteTo writes the envelope to the given writer in the Sentry envelope format.
|
||||
func (e *Envelope) WriteTo(w io.Writer) (int64, error) {
|
||||
data, err := e.Serialize()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
n, err := w.Write(data)
|
||||
return int64(n), err
|
||||
}
|
||||
|
||||
// writeItem writes a single envelope item to the buffer.
|
||||
func (e *Envelope) writeItem(buf *bytes.Buffer, item *EnvelopeItem) error {
|
||||
headerBytes, err := json.Marshal(item.Header)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal item header: %w", err)
|
||||
}
|
||||
|
||||
if _, err := buf.Write(headerBytes); err != nil {
|
||||
return fmt.Errorf("failed to write item header: %w", err)
|
||||
}
|
||||
|
||||
if _, err := buf.WriteString("\n"); err != nil {
|
||||
return fmt.Errorf("failed to write newline after item header: %w", err)
|
||||
}
|
||||
|
||||
if len(item.Payload) > 0 {
|
||||
if _, err := buf.Write(item.Payload); err != nil {
|
||||
return fmt.Errorf("failed to write item payload: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := buf.WriteString("\n"); err != nil {
|
||||
return fmt.Errorf("failed to write newline after item payload: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Size returns the total size of the envelope when serialized.
|
||||
func (e *Envelope) Size() (int, error) {
|
||||
data, err := e.Serialize()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(data), nil
|
||||
}
|
||||
|
||||
// MarshalJSON converts the EnvelopeHeader to JSON.
|
||||
func (h *EnvelopeHeader) MarshalJSON() ([]byte, error) {
|
||||
type header EnvelopeHeader
|
||||
return json.Marshal((*header)(h))
|
||||
}
|
||||
|
||||
// NewEnvelopeItem creates a new envelope item with the specified type and payload.
|
||||
func NewEnvelopeItem(itemType EnvelopeItemType, payload []byte) *EnvelopeItem {
|
||||
length := len(payload)
|
||||
return &EnvelopeItem{
|
||||
Header: &EnvelopeItemHeader{
|
||||
Type: itemType,
|
||||
Length: &length,
|
||||
},
|
||||
Payload: payload,
|
||||
}
|
||||
}
|
||||
|
||||
// NewAttachmentItem creates a new envelope item for an attachment.
|
||||
// Parameters: filename, contentType, payload.
|
||||
func NewAttachmentItem(filename, contentType string, payload []byte) *EnvelopeItem {
|
||||
length := len(payload)
|
||||
return &EnvelopeItem{
|
||||
Header: &EnvelopeItemHeader{
|
||||
Type: EnvelopeItemTypeAttachment,
|
||||
Length: &length,
|
||||
ContentType: contentType,
|
||||
Filename: filename,
|
||||
},
|
||||
Payload: payload,
|
||||
}
|
||||
}
|
||||
|
||||
// NewLogItem creates a new envelope item for logs.
|
||||
func NewLogItem(itemCount int, payload []byte) *EnvelopeItem {
|
||||
length := len(payload)
|
||||
return &EnvelopeItem{
|
||||
Header: &EnvelopeItemHeader{
|
||||
Type: EnvelopeItemTypeLog,
|
||||
Length: &length,
|
||||
ItemCount: &itemCount,
|
||||
ContentType: "application/vnd.sentry.items.log+json",
|
||||
},
|
||||
Payload: payload,
|
||||
}
|
||||
}
|
||||
|
||||
// NewTraceMetricItem creates a new envelope item for trace metrics.
|
||||
func NewTraceMetricItem(itemCount int, payload []byte) *EnvelopeItem {
|
||||
length := len(payload)
|
||||
return &EnvelopeItem{
|
||||
Header: &EnvelopeItemHeader{
|
||||
Type: EnvelopeItemTypeTraceMetric,
|
||||
Length: &length,
|
||||
ItemCount: &itemCount,
|
||||
ContentType: "application/vnd.sentry.items.trace-metric+json",
|
||||
},
|
||||
Payload: payload,
|
||||
}
|
||||
}
|
||||
56
vendor/github.com/getsentry/sentry-go/internal/protocol/interfaces.go
generated
vendored
Normal file
56
vendor/github.com/getsentry/sentry-go/internal/protocol/interfaces.go
generated
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/getsentry/sentry-go/internal/ratelimit"
|
||||
)
|
||||
|
||||
// TelemetryItem represents any telemetry data that can be stored in buffers and sent to Sentry.
|
||||
// This is the base interface that all telemetry items must implement.
|
||||
type TelemetryItem interface {
|
||||
// GetCategory returns the rate limit category for this item.
|
||||
GetCategory() ratelimit.Category
|
||||
|
||||
// GetEventID returns the event ID for this item.
|
||||
GetEventID() string
|
||||
|
||||
// GetSdkInfo returns SDK information for the envelope header.
|
||||
GetSdkInfo() *SdkInfo
|
||||
|
||||
// GetDynamicSamplingContext returns trace context for the envelope header.
|
||||
GetDynamicSamplingContext() map[string]string
|
||||
}
|
||||
|
||||
// EnvelopeItemConvertible represents items that can be converted directly to envelope items.
|
||||
type EnvelopeItemConvertible interface {
|
||||
TelemetryItem
|
||||
|
||||
// ToEnvelopeItem converts the item to a Sentry envelope item.
|
||||
ToEnvelopeItem() (*EnvelopeItem, error)
|
||||
}
|
||||
|
||||
// TelemetryTransport represents the envelope-first transport interface.
|
||||
// This interface is designed for the telemetry buffer system and provides
|
||||
// non-blocking sends with backpressure signals.
|
||||
type TelemetryTransport interface {
|
||||
// SendEnvelope sends an envelope to Sentry. Returns immediately with
|
||||
// backpressure error if the queue is full.
|
||||
SendEnvelope(envelope *Envelope) error
|
||||
|
||||
// HasCapacity reports whether the transport has capacity to accept at least one more envelope.
|
||||
HasCapacity() bool
|
||||
|
||||
// IsRateLimited checks if a specific category is currently rate limited
|
||||
IsRateLimited(category ratelimit.Category) bool
|
||||
|
||||
// Flush waits for all pending envelopes to be sent, with timeout
|
||||
Flush(timeout time.Duration) bool
|
||||
|
||||
// FlushWithContext waits for all pending envelopes to be sent
|
||||
FlushWithContext(ctx context.Context) bool
|
||||
|
||||
// Close shuts down the transport gracefully
|
||||
Close()
|
||||
}
|
||||
48
vendor/github.com/getsentry/sentry-go/internal/protocol/log_batch.go
generated
vendored
Normal file
48
vendor/github.com/getsentry/sentry-go/internal/protocol/log_batch.go
generated
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/getsentry/sentry-go/internal/ratelimit"
|
||||
)
|
||||
|
||||
// LogAttribute is the JSON representation for a single log attribute value.
|
||||
type LogAttribute struct {
|
||||
Value any `json:"value"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// Logs is a container for multiple log items which knows how to convert
|
||||
// itself into a single batched log envelope item.
|
||||
type Logs []TelemetryItem
|
||||
|
||||
func (ls Logs) ToEnvelopeItem() (*EnvelopeItem, error) {
|
||||
// Convert each log to its JSON representation
|
||||
items := make([]json.RawMessage, 0, len(ls))
|
||||
for _, log := range ls {
|
||||
logPayload, err := json.Marshal(log)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
items = append(items, logPayload)
|
||||
}
|
||||
|
||||
if len(items) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
wrapper := struct {
|
||||
Items []json.RawMessage `json:"items"`
|
||||
}{Items: items}
|
||||
|
||||
payload, err := json.Marshal(wrapper)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewLogItem(len(ls), payload), nil
|
||||
}
|
||||
|
||||
func (Logs) GetCategory() ratelimit.Category { return ratelimit.CategoryLog }
|
||||
func (Logs) GetEventID() string { return "" }
|
||||
func (Logs) GetSdkInfo() *SdkInfo { return nil }
|
||||
func (Logs) GetDynamicSamplingContext() map[string]string { return nil }
|
||||
41
vendor/github.com/getsentry/sentry-go/internal/protocol/metric_batch.go
generated
vendored
Normal file
41
vendor/github.com/getsentry/sentry-go/internal/protocol/metric_batch.go
generated
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/getsentry/sentry-go/internal/ratelimit"
|
||||
)
|
||||
|
||||
type Metrics []TelemetryItem
|
||||
|
||||
func (ms Metrics) ToEnvelopeItem() (*EnvelopeItem, error) {
|
||||
// Convert each metric to its JSON representation
|
||||
items := make([]json.RawMessage, 0, len(ms))
|
||||
for _, metric := range ms {
|
||||
metricPayload, err := json.Marshal(metric)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
items = append(items, metricPayload)
|
||||
}
|
||||
|
||||
if len(items) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
wrapper := struct {
|
||||
Items []json.RawMessage `json:"items"`
|
||||
}{Items: items}
|
||||
|
||||
payload, err := json.Marshal(wrapper)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewTraceMetricItem(len(items), payload), nil
|
||||
}
|
||||
|
||||
func (Metrics) GetCategory() ratelimit.Category { return ratelimit.CategoryTraceMetric }
|
||||
func (Metrics) GetEventID() string { return "" }
|
||||
func (Metrics) GetSdkInfo() *SdkInfo { return nil }
|
||||
func (Metrics) GetDynamicSamplingContext() map[string]string { return nil }
|
||||
15
vendor/github.com/getsentry/sentry-go/internal/protocol/types.go
generated
vendored
Normal file
15
vendor/github.com/getsentry/sentry-go/internal/protocol/types.go
generated
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
package protocol
|
||||
|
||||
// SdkInfo contains SDK metadata.
|
||||
type SdkInfo struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Integrations []string `json:"integrations,omitempty"`
|
||||
Packages []SdkPackage `json:"packages,omitempty"`
|
||||
}
|
||||
|
||||
// SdkPackage describes a package that was installed.
|
||||
type SdkPackage struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
}
|
||||
18
vendor/github.com/getsentry/sentry-go/internal/protocol/uuid.go
generated
vendored
Normal file
18
vendor/github.com/getsentry/sentry-go/internal/protocol/uuid.go
generated
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
)
|
||||
|
||||
// GenerateEventID generates a random UUID v4 for use as a Sentry event ID.
|
||||
func GenerateEventID() string {
|
||||
id := make([]byte, 16)
|
||||
// Prefer rand.Read over rand.Reader, see https://go-review.googlesource.com/c/go/+/272326/.
|
||||
_, _ = rand.Read(id)
|
||||
id[6] &= 0x0F // clear version
|
||||
id[6] |= 0x40 // set version to 4 (random uuid)
|
||||
id[8] &= 0x3F // clear variant
|
||||
id[8] |= 0x80 // set to IETF variant
|
||||
return hex.EncodeToString(id)
|
||||
}
|
||||
73
vendor/github.com/getsentry/sentry-go/internal/ratelimit/category.go
generated
vendored
73
vendor/github.com/getsentry/sentry-go/internal/ratelimit/category.go
generated
vendored
@@ -8,18 +8,21 @@ import (
|
||||
)
|
||||
|
||||
// Reference:
|
||||
// https://github.com/getsentry/relay/blob/0424a2e017d193a93918053c90cdae9472d164bf/relay-common/src/constants.rs#L116-L127
|
||||
// https://github.com/getsentry/relay/blob/46dfaa850b8717a6e22c3e9a275ba17fe673b9da/relay-base-schema/src/data_category.rs#L231-L271
|
||||
|
||||
// Category classifies supported payload types that can be ingested by Sentry
|
||||
// and, therefore, rate limited.
|
||||
type Category string
|
||||
|
||||
// Known rate limit categories. As a special case, the CategoryAll applies to
|
||||
// all known payload types.
|
||||
// Known rate limit categories that are specified in rate limit headers.
|
||||
const (
|
||||
CategoryAll Category = ""
|
||||
CategoryUnknown Category = "unknown" // Unknown category should not get rate limited
|
||||
CategoryAll Category = "" // Special category for empty categories (applies to all)
|
||||
CategoryError Category = "error"
|
||||
CategoryTransaction Category = "transaction"
|
||||
CategoryLog Category = "log_item"
|
||||
CategoryMonitor Category = "monitor"
|
||||
CategoryTraceMetric Category = "trace_metric"
|
||||
)
|
||||
|
||||
// knownCategories is the set of currently known categories. Other categories
|
||||
@@ -28,14 +31,28 @@ var knownCategories = map[Category]struct{}{
|
||||
CategoryAll: {},
|
||||
CategoryError: {},
|
||||
CategoryTransaction: {},
|
||||
CategoryLog: {},
|
||||
CategoryMonitor: {},
|
||||
CategoryTraceMetric: {},
|
||||
}
|
||||
|
||||
// String returns the category formatted for debugging.
|
||||
func (c Category) String() string {
|
||||
switch c {
|
||||
case "":
|
||||
case CategoryAll:
|
||||
return "CategoryAll"
|
||||
case CategoryError:
|
||||
return "CategoryError"
|
||||
case CategoryTransaction:
|
||||
return "CategoryTransaction"
|
||||
case CategoryLog:
|
||||
return "CategoryLog"
|
||||
case CategoryMonitor:
|
||||
return "CategoryMonitor"
|
||||
case CategoryTraceMetric:
|
||||
return "CategoryTraceMetric"
|
||||
default:
|
||||
// For unknown categories, use the original formatting logic
|
||||
caser := cases.Title(language.English)
|
||||
rv := "Category"
|
||||
for _, w := range strings.Fields(string(c)) {
|
||||
@@ -44,3 +61,49 @@ func (c Category) String() string {
|
||||
return rv
|
||||
}
|
||||
}
|
||||
|
||||
// Priority represents the importance level of a category for buffer management.
|
||||
type Priority int
|
||||
|
||||
const (
|
||||
PriorityCritical Priority = iota + 1
|
||||
PriorityHigh
|
||||
PriorityMedium
|
||||
PriorityLow
|
||||
PriorityLowest
|
||||
)
|
||||
|
||||
func (p Priority) String() string {
|
||||
switch p {
|
||||
case PriorityCritical:
|
||||
return "critical"
|
||||
case PriorityHigh:
|
||||
return "high"
|
||||
case PriorityMedium:
|
||||
return "medium"
|
||||
case PriorityLow:
|
||||
return "low"
|
||||
case PriorityLowest:
|
||||
return "lowest"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// GetPriority returns the priority level for this category.
|
||||
func (c Category) GetPriority() Priority {
|
||||
switch c {
|
||||
case CategoryError:
|
||||
return PriorityCritical
|
||||
case CategoryMonitor:
|
||||
return PriorityHigh
|
||||
case CategoryLog:
|
||||
return PriorityLow
|
||||
case CategoryTransaction:
|
||||
return PriorityMedium
|
||||
case CategoryTraceMetric:
|
||||
return PriorityLow
|
||||
default:
|
||||
return PriorityMedium
|
||||
}
|
||||
}
|
||||
|
||||
398
vendor/github.com/getsentry/sentry-go/internal/telemetry/bucketed_buffer.go
generated
vendored
Normal file
398
vendor/github.com/getsentry/sentry-go/internal/telemetry/bucketed_buffer.go
generated
vendored
Normal file
@@ -0,0 +1,398 @@
|
||||
package telemetry
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/getsentry/sentry-go/internal/ratelimit"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultBucketedCapacity = 100
|
||||
perBucketItemLimit = 100
|
||||
)
|
||||
|
||||
type Bucket[T any] struct {
|
||||
traceID string
|
||||
items []T
|
||||
createdAt time.Time
|
||||
lastUpdatedAt time.Time
|
||||
}
|
||||
|
||||
// BucketedBuffer groups items by trace id, flushing per bucket.
|
||||
type BucketedBuffer[T any] struct {
|
||||
mu sync.RWMutex
|
||||
|
||||
buckets []*Bucket[T]
|
||||
traceIndex map[string]int
|
||||
|
||||
head int
|
||||
tail int
|
||||
|
||||
itemCapacity int
|
||||
bucketCapacity int
|
||||
|
||||
totalItems int
|
||||
bucketCount int
|
||||
|
||||
category ratelimit.Category
|
||||
priority ratelimit.Priority
|
||||
overflowPolicy OverflowPolicy
|
||||
batchSize int
|
||||
timeout time.Duration
|
||||
lastFlushTime time.Time
|
||||
|
||||
offered int64
|
||||
dropped int64
|
||||
onDropped func(item T, reason string)
|
||||
}
|
||||
|
||||
func NewBucketedBuffer[T any](
|
||||
category ratelimit.Category,
|
||||
capacity int,
|
||||
overflowPolicy OverflowPolicy,
|
||||
batchSize int,
|
||||
timeout time.Duration,
|
||||
) *BucketedBuffer[T] {
|
||||
if capacity <= 0 {
|
||||
capacity = defaultBucketedCapacity
|
||||
}
|
||||
if batchSize <= 0 {
|
||||
batchSize = 1
|
||||
}
|
||||
if timeout < 0 {
|
||||
timeout = 0
|
||||
}
|
||||
|
||||
bucketCapacity := capacity / 10
|
||||
if bucketCapacity < 10 {
|
||||
bucketCapacity = 10
|
||||
}
|
||||
|
||||
return &BucketedBuffer[T]{
|
||||
buckets: make([]*Bucket[T], bucketCapacity),
|
||||
traceIndex: make(map[string]int),
|
||||
itemCapacity: capacity,
|
||||
bucketCapacity: bucketCapacity,
|
||||
category: category,
|
||||
priority: category.GetPriority(),
|
||||
overflowPolicy: overflowPolicy,
|
||||
batchSize: batchSize,
|
||||
timeout: timeout,
|
||||
lastFlushTime: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BucketedBuffer[T]) Offer(item T) bool {
|
||||
atomic.AddInt64(&b.offered, 1)
|
||||
|
||||
traceID := ""
|
||||
if ta, ok := any(item).(TraceAware); ok {
|
||||
if tid, hasTrace := ta.GetTraceID(); hasTrace {
|
||||
traceID = tid
|
||||
}
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return b.offerToBucket(item, traceID)
|
||||
}
|
||||
|
||||
func (b *BucketedBuffer[T]) offerToBucket(item T, traceID string) bool {
|
||||
if traceID != "" {
|
||||
if idx, exists := b.traceIndex[traceID]; exists {
|
||||
bucket := b.buckets[idx]
|
||||
if len(bucket.items) >= perBucketItemLimit {
|
||||
delete(b.traceIndex, traceID)
|
||||
} else {
|
||||
bucket.items = append(bucket.items, item)
|
||||
bucket.lastUpdatedAt = time.Now()
|
||||
b.totalItems++
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if b.totalItems >= b.itemCapacity {
|
||||
return b.handleOverflow(item, traceID)
|
||||
}
|
||||
if b.bucketCount >= b.bucketCapacity {
|
||||
return b.handleOverflow(item, traceID)
|
||||
}
|
||||
|
||||
bucket := &Bucket[T]{
|
||||
traceID: traceID,
|
||||
items: []T{item},
|
||||
createdAt: time.Now(),
|
||||
lastUpdatedAt: time.Now(),
|
||||
}
|
||||
b.buckets[b.tail] = bucket
|
||||
if traceID != "" {
|
||||
b.traceIndex[traceID] = b.tail
|
||||
}
|
||||
b.tail = (b.tail + 1) % b.bucketCapacity
|
||||
b.bucketCount++
|
||||
b.totalItems++
|
||||
return true
|
||||
}
|
||||
|
||||
func (b *BucketedBuffer[T]) handleOverflow(item T, traceID string) bool {
|
||||
switch b.overflowPolicy {
|
||||
case OverflowPolicyDropOldest:
|
||||
oldestBucket := b.buckets[b.head]
|
||||
if oldestBucket == nil {
|
||||
atomic.AddInt64(&b.dropped, 1)
|
||||
if b.onDropped != nil {
|
||||
b.onDropped(item, "buffer_full_invalid_state")
|
||||
}
|
||||
return false
|
||||
}
|
||||
if oldestBucket.traceID != "" {
|
||||
delete(b.traceIndex, oldestBucket.traceID)
|
||||
}
|
||||
droppedCount := len(oldestBucket.items)
|
||||
atomic.AddInt64(&b.dropped, int64(droppedCount))
|
||||
if b.onDropped != nil {
|
||||
for _, di := range oldestBucket.items {
|
||||
b.onDropped(di, "buffer_full_drop_oldest_bucket")
|
||||
}
|
||||
}
|
||||
b.totalItems -= droppedCount
|
||||
b.bucketCount--
|
||||
b.head = (b.head + 1) % b.bucketCapacity
|
||||
// add new bucket
|
||||
bucket := &Bucket[T]{traceID: traceID, items: []T{item}, createdAt: time.Now(), lastUpdatedAt: time.Now()}
|
||||
b.buckets[b.tail] = bucket
|
||||
if traceID != "" {
|
||||
b.traceIndex[traceID] = b.tail
|
||||
}
|
||||
b.tail = (b.tail + 1) % b.bucketCapacity
|
||||
b.bucketCount++
|
||||
b.totalItems++
|
||||
return true
|
||||
case OverflowPolicyDropNewest:
|
||||
atomic.AddInt64(&b.dropped, 1)
|
||||
if b.onDropped != nil {
|
||||
b.onDropped(item, "buffer_full_drop_newest")
|
||||
}
|
||||
return false
|
||||
default:
|
||||
atomic.AddInt64(&b.dropped, 1)
|
||||
if b.onDropped != nil {
|
||||
b.onDropped(item, "unknown_overflow_policy")
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BucketedBuffer[T]) Poll() (T, bool) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
var zero T
|
||||
if b.bucketCount == 0 {
|
||||
return zero, false
|
||||
}
|
||||
bucket := b.buckets[b.head]
|
||||
if bucket == nil || len(bucket.items) == 0 {
|
||||
return zero, false
|
||||
}
|
||||
item := bucket.items[0]
|
||||
bucket.items = bucket.items[1:]
|
||||
b.totalItems--
|
||||
if len(bucket.items) == 0 {
|
||||
if bucket.traceID != "" {
|
||||
delete(b.traceIndex, bucket.traceID)
|
||||
}
|
||||
b.buckets[b.head] = nil
|
||||
b.head = (b.head + 1) % b.bucketCapacity
|
||||
b.bucketCount--
|
||||
}
|
||||
return item, true
|
||||
}
|
||||
|
||||
func (b *BucketedBuffer[T]) PollBatch(maxItems int) []T {
|
||||
if maxItems <= 0 {
|
||||
return nil
|
||||
}
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if b.bucketCount == 0 {
|
||||
return nil
|
||||
}
|
||||
res := make([]T, 0, maxItems)
|
||||
for len(res) < maxItems && b.bucketCount > 0 {
|
||||
bucket := b.buckets[b.head]
|
||||
if bucket == nil {
|
||||
break
|
||||
}
|
||||
n := maxItems - len(res)
|
||||
if n > len(bucket.items) {
|
||||
n = len(bucket.items)
|
||||
}
|
||||
res = append(res, bucket.items[:n]...)
|
||||
bucket.items = bucket.items[n:]
|
||||
b.totalItems -= n
|
||||
if len(bucket.items) == 0 {
|
||||
if bucket.traceID != "" {
|
||||
delete(b.traceIndex, bucket.traceID)
|
||||
}
|
||||
b.buckets[b.head] = nil
|
||||
b.head = (b.head + 1) % b.bucketCapacity
|
||||
b.bucketCount--
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (b *BucketedBuffer[T]) PollIfReady() []T {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if b.bucketCount == 0 {
|
||||
return nil
|
||||
}
|
||||
ready := b.totalItems >= b.batchSize || (b.timeout > 0 && time.Since(b.lastFlushTime) >= b.timeout)
|
||||
if !ready {
|
||||
return nil
|
||||
}
|
||||
oldest := b.buckets[b.head]
|
||||
if oldest == nil {
|
||||
return nil
|
||||
}
|
||||
items := oldest.items
|
||||
if oldest.traceID != "" {
|
||||
delete(b.traceIndex, oldest.traceID)
|
||||
}
|
||||
b.buckets[b.head] = nil
|
||||
b.head = (b.head + 1) % b.bucketCapacity
|
||||
b.totalItems -= len(items)
|
||||
b.bucketCount--
|
||||
b.lastFlushTime = time.Now()
|
||||
return items
|
||||
}
|
||||
|
||||
func (b *BucketedBuffer[T]) Drain() []T {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if b.bucketCount == 0 {
|
||||
return nil
|
||||
}
|
||||
res := make([]T, 0, b.totalItems)
|
||||
for i := 0; i < b.bucketCount; i++ {
|
||||
idx := (b.head + i) % b.bucketCapacity
|
||||
bucket := b.buckets[idx]
|
||||
if bucket != nil {
|
||||
res = append(res, bucket.items...)
|
||||
b.buckets[idx] = nil
|
||||
}
|
||||
}
|
||||
b.traceIndex = make(map[string]int)
|
||||
b.head = 0
|
||||
b.tail = 0
|
||||
b.totalItems = 0
|
||||
b.bucketCount = 0
|
||||
return res
|
||||
}
|
||||
|
||||
func (b *BucketedBuffer[T]) Peek() (T, bool) {
|
||||
b.mu.RLock()
|
||||
defer b.mu.RUnlock()
|
||||
var zero T
|
||||
if b.bucketCount == 0 {
|
||||
return zero, false
|
||||
}
|
||||
bucket := b.buckets[b.head]
|
||||
if bucket == nil || len(bucket.items) == 0 {
|
||||
return zero, false
|
||||
}
|
||||
return bucket.items[0], true
|
||||
}
|
||||
|
||||
func (b *BucketedBuffer[T]) Size() int { b.mu.RLock(); defer b.mu.RUnlock(); return b.totalItems }
|
||||
func (b *BucketedBuffer[T]) Capacity() int { b.mu.RLock(); defer b.mu.RUnlock(); return b.itemCapacity }
|
||||
func (b *BucketedBuffer[T]) Category() ratelimit.Category {
|
||||
b.mu.RLock()
|
||||
defer b.mu.RUnlock()
|
||||
return b.category
|
||||
}
|
||||
func (b *BucketedBuffer[T]) Priority() ratelimit.Priority {
|
||||
b.mu.RLock()
|
||||
defer b.mu.RUnlock()
|
||||
return b.priority
|
||||
}
|
||||
func (b *BucketedBuffer[T]) IsEmpty() bool {
|
||||
b.mu.RLock()
|
||||
defer b.mu.RUnlock()
|
||||
return b.bucketCount == 0
|
||||
}
|
||||
func (b *BucketedBuffer[T]) IsFull() bool {
|
||||
b.mu.RLock()
|
||||
defer b.mu.RUnlock()
|
||||
return b.totalItems >= b.itemCapacity
|
||||
}
|
||||
func (b *BucketedBuffer[T]) Utilization() float64 {
|
||||
b.mu.RLock()
|
||||
defer b.mu.RUnlock()
|
||||
if b.itemCapacity == 0 {
|
||||
return 0
|
||||
}
|
||||
return float64(b.totalItems) / float64(b.itemCapacity)
|
||||
}
|
||||
func (b *BucketedBuffer[T]) OfferedCount() int64 { return atomic.LoadInt64(&b.offered) }
|
||||
func (b *BucketedBuffer[T]) DroppedCount() int64 { return atomic.LoadInt64(&b.dropped) }
|
||||
func (b *BucketedBuffer[T]) AcceptedCount() int64 { return b.OfferedCount() - b.DroppedCount() }
|
||||
func (b *BucketedBuffer[T]) DropRate() float64 {
|
||||
off := b.OfferedCount()
|
||||
if off == 0 {
|
||||
return 0
|
||||
}
|
||||
return float64(b.DroppedCount()) / float64(off)
|
||||
}
|
||||
|
||||
func (b *BucketedBuffer[T]) GetMetrics() BufferMetrics {
|
||||
b.mu.RLock()
|
||||
size := b.totalItems
|
||||
util := 0.0
|
||||
if b.itemCapacity > 0 {
|
||||
util = float64(b.totalItems) / float64(b.itemCapacity)
|
||||
}
|
||||
b.mu.RUnlock()
|
||||
return BufferMetrics{Category: b.category, Priority: b.priority, Capacity: b.itemCapacity, Size: size, Utilization: util, OfferedCount: b.OfferedCount(), DroppedCount: b.DroppedCount(), AcceptedCount: b.AcceptedCount(), DropRate: b.DropRate(), LastUpdated: time.Now()}
|
||||
}
|
||||
|
||||
func (b *BucketedBuffer[T]) SetDroppedCallback(callback func(item T, reason string)) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
b.onDropped = callback
|
||||
}
|
||||
func (b *BucketedBuffer[T]) Clear() {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
for i := 0; i < b.bucketCapacity; i++ {
|
||||
b.buckets[i] = nil
|
||||
}
|
||||
b.traceIndex = make(map[string]int)
|
||||
b.head = 0
|
||||
b.tail = 0
|
||||
b.totalItems = 0
|
||||
b.bucketCount = 0
|
||||
}
|
||||
func (b *BucketedBuffer[T]) IsReadyToFlush() bool {
|
||||
b.mu.RLock()
|
||||
defer b.mu.RUnlock()
|
||||
if b.bucketCount == 0 {
|
||||
return false
|
||||
}
|
||||
if b.totalItems >= b.batchSize {
|
||||
return true
|
||||
}
|
||||
if b.timeout > 0 && time.Since(b.lastFlushTime) >= b.timeout {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
func (b *BucketedBuffer[T]) MarkFlushed() {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
b.lastFlushTime = time.Now()
|
||||
}
|
||||
378
vendor/github.com/getsentry/sentry-go/internal/telemetry/buffer.go
generated
vendored
Normal file
378
vendor/github.com/getsentry/sentry-go/internal/telemetry/buffer.go
generated
vendored
Normal file
@@ -0,0 +1,378 @@
|
||||
package telemetry
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/getsentry/sentry-go/internal/ratelimit"
|
||||
)
|
||||
|
||||
const defaultCapacity = 100
|
||||
|
||||
// RingBuffer is a thread-safe ring buffer with overflow policies.
|
||||
type RingBuffer[T any] struct {
|
||||
mu sync.RWMutex
|
||||
items []T
|
||||
head int
|
||||
tail int
|
||||
size int
|
||||
capacity int
|
||||
|
||||
category ratelimit.Category
|
||||
priority ratelimit.Priority
|
||||
overflowPolicy OverflowPolicy
|
||||
|
||||
batchSize int
|
||||
timeout time.Duration
|
||||
lastFlushTime time.Time
|
||||
|
||||
offered int64
|
||||
dropped int64
|
||||
onDropped func(item T, reason string)
|
||||
}
|
||||
|
||||
func NewRingBuffer[T any](category ratelimit.Category, capacity int, overflowPolicy OverflowPolicy, batchSize int, timeout time.Duration) *RingBuffer[T] {
|
||||
if capacity <= 0 {
|
||||
capacity = defaultCapacity
|
||||
}
|
||||
|
||||
if batchSize <= 0 {
|
||||
batchSize = 1
|
||||
}
|
||||
|
||||
if timeout < 0 {
|
||||
timeout = 0
|
||||
}
|
||||
|
||||
return &RingBuffer[T]{
|
||||
items: make([]T, capacity),
|
||||
capacity: capacity,
|
||||
category: category,
|
||||
priority: category.GetPriority(),
|
||||
overflowPolicy: overflowPolicy,
|
||||
batchSize: batchSize,
|
||||
timeout: timeout,
|
||||
lastFlushTime: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func (b *RingBuffer[T]) SetDroppedCallback(callback func(item T, reason string)) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
b.onDropped = callback
|
||||
}
|
||||
|
||||
func (b *RingBuffer[T]) Offer(item T) bool {
|
||||
atomic.AddInt64(&b.offered, 1)
|
||||
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
if b.size < b.capacity {
|
||||
b.items[b.tail] = item
|
||||
b.tail = (b.tail + 1) % b.capacity
|
||||
b.size++
|
||||
return true
|
||||
}
|
||||
|
||||
switch b.overflowPolicy {
|
||||
case OverflowPolicyDropOldest:
|
||||
oldItem := b.items[b.head]
|
||||
b.items[b.head] = item
|
||||
b.head = (b.head + 1) % b.capacity
|
||||
b.tail = (b.tail + 1) % b.capacity
|
||||
|
||||
atomic.AddInt64(&b.dropped, 1)
|
||||
if b.onDropped != nil {
|
||||
b.onDropped(oldItem, "buffer_full_drop_oldest")
|
||||
}
|
||||
return true
|
||||
|
||||
case OverflowPolicyDropNewest:
|
||||
atomic.AddInt64(&b.dropped, 1)
|
||||
if b.onDropped != nil {
|
||||
b.onDropped(item, "buffer_full_drop_newest")
|
||||
}
|
||||
return false
|
||||
|
||||
default:
|
||||
atomic.AddInt64(&b.dropped, 1)
|
||||
if b.onDropped != nil {
|
||||
b.onDropped(item, "unknown_overflow_policy")
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (b *RingBuffer[T]) Poll() (T, bool) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
var zero T
|
||||
if b.size == 0 {
|
||||
return zero, false
|
||||
}
|
||||
|
||||
item := b.items[b.head]
|
||||
b.items[b.head] = zero
|
||||
b.head = (b.head + 1) % b.capacity
|
||||
b.size--
|
||||
|
||||
return item, true
|
||||
}
|
||||
|
||||
func (b *RingBuffer[T]) PollBatch(maxItems int) []T {
|
||||
if maxItems <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
if b.size == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
itemCount := maxItems
|
||||
if itemCount > b.size {
|
||||
itemCount = b.size
|
||||
}
|
||||
|
||||
result := make([]T, itemCount)
|
||||
var zero T
|
||||
|
||||
for i := 0; i < itemCount; i++ {
|
||||
result[i] = b.items[b.head]
|
||||
b.items[b.head] = zero
|
||||
b.head = (b.head + 1) % b.capacity
|
||||
b.size--
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (b *RingBuffer[T]) Drain() []T {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
if b.size == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := make([]T, b.size)
|
||||
index := 0
|
||||
var zero T
|
||||
|
||||
for i := 0; i < b.size; i++ {
|
||||
pos := (b.head + i) % b.capacity
|
||||
result[index] = b.items[pos]
|
||||
b.items[pos] = zero
|
||||
index++
|
||||
}
|
||||
|
||||
b.head = 0
|
||||
b.tail = 0
|
||||
b.size = 0
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (b *RingBuffer[T]) Peek() (T, bool) {
|
||||
b.mu.RLock()
|
||||
defer b.mu.RUnlock()
|
||||
|
||||
var zero T
|
||||
if b.size == 0 {
|
||||
return zero, false
|
||||
}
|
||||
|
||||
return b.items[b.head], true
|
||||
}
|
||||
|
||||
func (b *RingBuffer[T]) Size() int {
|
||||
b.mu.RLock()
|
||||
defer b.mu.RUnlock()
|
||||
return b.size
|
||||
}
|
||||
|
||||
func (b *RingBuffer[T]) Capacity() int {
|
||||
b.mu.RLock()
|
||||
defer b.mu.RUnlock()
|
||||
return b.capacity
|
||||
}
|
||||
|
||||
func (b *RingBuffer[T]) Category() ratelimit.Category {
|
||||
b.mu.RLock()
|
||||
defer b.mu.RUnlock()
|
||||
return b.category
|
||||
}
|
||||
|
||||
func (b *RingBuffer[T]) Priority() ratelimit.Priority {
|
||||
b.mu.RLock()
|
||||
defer b.mu.RUnlock()
|
||||
return b.priority
|
||||
}
|
||||
|
||||
func (b *RingBuffer[T]) IsEmpty() bool {
|
||||
b.mu.RLock()
|
||||
defer b.mu.RUnlock()
|
||||
return b.size == 0
|
||||
}
|
||||
|
||||
func (b *RingBuffer[T]) IsFull() bool {
|
||||
b.mu.RLock()
|
||||
defer b.mu.RUnlock()
|
||||
return b.size == b.capacity
|
||||
}
|
||||
|
||||
func (b *RingBuffer[T]) Utilization() float64 {
|
||||
b.mu.RLock()
|
||||
defer b.mu.RUnlock()
|
||||
return float64(b.size) / float64(b.capacity)
|
||||
}
|
||||
|
||||
func (b *RingBuffer[T]) OfferedCount() int64 {
|
||||
return atomic.LoadInt64(&b.offered)
|
||||
}
|
||||
|
||||
func (b *RingBuffer[T]) DroppedCount() int64 {
|
||||
return atomic.LoadInt64(&b.dropped)
|
||||
}
|
||||
|
||||
func (b *RingBuffer[T]) AcceptedCount() int64 {
|
||||
return b.OfferedCount() - b.DroppedCount()
|
||||
}
|
||||
|
||||
func (b *RingBuffer[T]) DropRate() float64 {
|
||||
offered := b.OfferedCount()
|
||||
if offered == 0 {
|
||||
return 0.0
|
||||
}
|
||||
return float64(b.DroppedCount()) / float64(offered)
|
||||
}
|
||||
|
||||
func (b *RingBuffer[T]) Clear() {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
var zero T
|
||||
for i := 0; i < b.capacity; i++ {
|
||||
b.items[i] = zero
|
||||
}
|
||||
|
||||
b.head = 0
|
||||
b.tail = 0
|
||||
b.size = 0
|
||||
}
|
||||
|
||||
func (b *RingBuffer[T]) GetMetrics() BufferMetrics {
|
||||
b.mu.RLock()
|
||||
size := b.size
|
||||
util := float64(b.size) / float64(b.capacity)
|
||||
b.mu.RUnlock()
|
||||
|
||||
return BufferMetrics{
|
||||
Category: b.category,
|
||||
Priority: b.priority,
|
||||
Capacity: b.capacity,
|
||||
Size: size,
|
||||
Utilization: util,
|
||||
OfferedCount: b.OfferedCount(),
|
||||
DroppedCount: b.DroppedCount(),
|
||||
AcceptedCount: b.AcceptedCount(),
|
||||
DropRate: b.DropRate(),
|
||||
LastUpdated: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func (b *RingBuffer[T]) IsReadyToFlush() bool {
|
||||
b.mu.RLock()
|
||||
defer b.mu.RUnlock()
|
||||
|
||||
if b.size == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
if b.size >= b.batchSize {
|
||||
return true
|
||||
}
|
||||
|
||||
if b.timeout > 0 && time.Since(b.lastFlushTime) >= b.timeout {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (b *RingBuffer[T]) MarkFlushed() {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
b.lastFlushTime = time.Now()
|
||||
}
|
||||
|
||||
func (b *RingBuffer[T]) PollIfReady() []T {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
if b.size == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
ready := b.size >= b.batchSize ||
|
||||
(b.timeout > 0 && time.Since(b.lastFlushTime) >= b.timeout)
|
||||
|
||||
if !ready {
|
||||
return nil
|
||||
}
|
||||
|
||||
itemCount := b.batchSize
|
||||
if itemCount > b.size {
|
||||
itemCount = b.size
|
||||
}
|
||||
|
||||
result := make([]T, itemCount)
|
||||
var zero T
|
||||
|
||||
for i := 0; i < itemCount; i++ {
|
||||
result[i] = b.items[b.head]
|
||||
b.items[b.head] = zero
|
||||
b.head = (b.head + 1) % b.capacity
|
||||
b.size--
|
||||
}
|
||||
|
||||
b.lastFlushTime = time.Now()
|
||||
return result
|
||||
}
|
||||
|
||||
type BufferMetrics struct {
|
||||
Category ratelimit.Category `json:"category"`
|
||||
Priority ratelimit.Priority `json:"priority"`
|
||||
Capacity int `json:"capacity"`
|
||||
Size int `json:"size"`
|
||||
Utilization float64 `json:"utilization"`
|
||||
OfferedCount int64 `json:"offered_count"`
|
||||
DroppedCount int64 `json:"dropped_count"`
|
||||
AcceptedCount int64 `json:"accepted_count"`
|
||||
DropRate float64 `json:"drop_rate"`
|
||||
LastUpdated time.Time `json:"last_updated"`
|
||||
}
|
||||
|
||||
// OverflowPolicy defines how the ring buffer handles overflow.
|
||||
type OverflowPolicy int
|
||||
|
||||
const (
|
||||
OverflowPolicyDropOldest OverflowPolicy = iota
|
||||
OverflowPolicyDropNewest
|
||||
)
|
||||
|
||||
func (op OverflowPolicy) String() string {
|
||||
switch op {
|
||||
case OverflowPolicyDropOldest:
|
||||
return "drop_oldest"
|
||||
case OverflowPolicyDropNewest:
|
||||
return "drop_newest"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
49
vendor/github.com/getsentry/sentry-go/internal/telemetry/buffer_wrapper.go
generated
vendored
Normal file
49
vendor/github.com/getsentry/sentry-go/internal/telemetry/buffer_wrapper.go
generated
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
package telemetry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/getsentry/sentry-go/internal/protocol"
|
||||
"github.com/getsentry/sentry-go/internal/ratelimit"
|
||||
)
|
||||
|
||||
// Processor is the top-level object that wraps the scheduler and buffers.
|
||||
type Processor struct {
|
||||
scheduler *Scheduler
|
||||
}
|
||||
|
||||
// NewProcessor creates a new Processor with the given configuration.
|
||||
func NewProcessor(
|
||||
buffers map[ratelimit.Category]Buffer[protocol.TelemetryItem],
|
||||
transport protocol.TelemetryTransport,
|
||||
dsn *protocol.Dsn,
|
||||
sdkInfo *protocol.SdkInfo,
|
||||
) *Processor {
|
||||
scheduler := NewScheduler(buffers, transport, dsn, sdkInfo)
|
||||
scheduler.Start()
|
||||
|
||||
return &Processor{
|
||||
scheduler: scheduler,
|
||||
}
|
||||
}
|
||||
|
||||
// Add adds a TelemetryItem to the appropriate buffer based on its category.
|
||||
func (b *Processor) Add(item protocol.TelemetryItem) bool {
|
||||
return b.scheduler.Add(item)
|
||||
}
|
||||
|
||||
// Flush forces all buffers to flush within the given timeout.
|
||||
func (b *Processor) Flush(timeout time.Duration) bool {
|
||||
return b.scheduler.Flush(timeout)
|
||||
}
|
||||
|
||||
// FlushWithContext flushes with a custom context for cancellation.
|
||||
func (b *Processor) FlushWithContext(ctx context.Context) bool {
|
||||
return b.scheduler.FlushWithContext(ctx)
|
||||
}
|
||||
|
||||
// Close stops the buffer, flushes remaining data, and releases resources.
|
||||
func (b *Processor) Close(timeout time.Duration) {
|
||||
b.scheduler.Stop(timeout)
|
||||
}
|
||||
301
vendor/github.com/getsentry/sentry-go/internal/telemetry/scheduler.go
generated
vendored
Normal file
301
vendor/github.com/getsentry/sentry-go/internal/telemetry/scheduler.go
generated
vendored
Normal file
@@ -0,0 +1,301 @@
|
||||
package telemetry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/getsentry/sentry-go/internal/debuglog"
|
||||
"github.com/getsentry/sentry-go/internal/protocol"
|
||||
"github.com/getsentry/sentry-go/internal/ratelimit"
|
||||
)
|
||||
|
||||
// Scheduler implements a weighted round-robin scheduler for processing buffered events.
|
||||
type Scheduler struct {
|
||||
buffers map[ratelimit.Category]Buffer[protocol.TelemetryItem]
|
||||
transport protocol.TelemetryTransport
|
||||
dsn *protocol.Dsn
|
||||
sdkInfo *protocol.SdkInfo
|
||||
|
||||
currentCycle []ratelimit.Priority
|
||||
cyclePos int
|
||||
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
processingWg sync.WaitGroup
|
||||
|
||||
mu sync.Mutex
|
||||
cond *sync.Cond
|
||||
startOnce sync.Once
|
||||
finishOnce sync.Once
|
||||
}
|
||||
|
||||
func NewScheduler(
|
||||
buffers map[ratelimit.Category]Buffer[protocol.TelemetryItem],
|
||||
transport protocol.TelemetryTransport,
|
||||
dsn *protocol.Dsn,
|
||||
sdkInfo *protocol.SdkInfo,
|
||||
) *Scheduler {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
priorityWeights := map[ratelimit.Priority]int{
|
||||
ratelimit.PriorityCritical: 5,
|
||||
ratelimit.PriorityHigh: 4,
|
||||
ratelimit.PriorityMedium: 3,
|
||||
ratelimit.PriorityLow: 2,
|
||||
ratelimit.PriorityLowest: 1,
|
||||
}
|
||||
|
||||
var currentCycle []ratelimit.Priority
|
||||
for priority, weight := range priorityWeights {
|
||||
hasBuffers := false
|
||||
for _, buffer := range buffers {
|
||||
if buffer.Priority() == priority {
|
||||
hasBuffers = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if hasBuffers {
|
||||
for i := 0; i < weight; i++ {
|
||||
currentCycle = append(currentCycle, priority)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
s := &Scheduler{
|
||||
buffers: buffers,
|
||||
transport: transport,
|
||||
dsn: dsn,
|
||||
sdkInfo: sdkInfo,
|
||||
currentCycle: currentCycle,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
s.cond = sync.NewCond(&s.mu)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Scheduler) Start() {
|
||||
s.startOnce.Do(func() {
|
||||
s.processingWg.Add(1)
|
||||
go s.run()
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Scheduler) Stop(timeout time.Duration) {
|
||||
s.finishOnce.Do(func() {
|
||||
s.Flush(timeout)
|
||||
|
||||
s.cancel()
|
||||
s.cond.Broadcast()
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
s.processingWg.Wait()
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(timeout):
|
||||
debuglog.Printf("scheduler stop timed out after %v", timeout)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Scheduler) Signal() {
|
||||
s.cond.Signal()
|
||||
}
|
||||
|
||||
func (s *Scheduler) Add(item protocol.TelemetryItem) bool {
|
||||
category := item.GetCategory()
|
||||
buffer, exists := s.buffers[category]
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
|
||||
accepted := buffer.Offer(item)
|
||||
if accepted {
|
||||
s.Signal()
|
||||
}
|
||||
return accepted
|
||||
}
|
||||
|
||||
func (s *Scheduler) Flush(timeout time.Duration) bool {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
return s.FlushWithContext(ctx)
|
||||
}
|
||||
|
||||
func (s *Scheduler) FlushWithContext(ctx context.Context) bool {
|
||||
s.flushBuffers()
|
||||
return s.transport.FlushWithContext(ctx)
|
||||
}
|
||||
|
||||
func (s *Scheduler) run() {
|
||||
defer s.processingWg.Done()
|
||||
|
||||
go func() {
|
||||
ticker := time.NewTicker(100 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
s.cond.Broadcast()
|
||||
case <-s.ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
s.mu.Lock()
|
||||
|
||||
for !s.hasWork() && s.ctx.Err() == nil {
|
||||
s.cond.Wait()
|
||||
}
|
||||
|
||||
if s.ctx.Err() != nil {
|
||||
s.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
s.mu.Unlock()
|
||||
s.processNextBatch()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Scheduler) hasWork() bool {
|
||||
for _, buffer := range s.buffers {
|
||||
if buffer.IsReadyToFlush() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *Scheduler) processNextBatch() {
|
||||
if len(s.currentCycle) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
priority := s.currentCycle[s.cyclePos]
|
||||
s.cyclePos = (s.cyclePos + 1) % len(s.currentCycle)
|
||||
|
||||
var bufferToProcess Buffer[protocol.TelemetryItem]
|
||||
var categoryToProcess ratelimit.Category
|
||||
for category, buffer := range s.buffers {
|
||||
if buffer.Priority() == priority && buffer.IsReadyToFlush() {
|
||||
bufferToProcess = buffer
|
||||
categoryToProcess = category
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if bufferToProcess != nil {
|
||||
s.processItems(bufferToProcess, categoryToProcess, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Scheduler) processItems(buffer Buffer[protocol.TelemetryItem], category ratelimit.Category, force bool) {
|
||||
var items []protocol.TelemetryItem
|
||||
|
||||
if force {
|
||||
items = buffer.Drain()
|
||||
} else {
|
||||
items = buffer.PollIfReady()
|
||||
}
|
||||
|
||||
// drop the current batch if rate-limited or if transport is full
|
||||
if len(items) == 0 || s.isRateLimited(category) || !s.transport.HasCapacity() {
|
||||
return
|
||||
}
|
||||
|
||||
switch category {
|
||||
case ratelimit.CategoryLog:
|
||||
logs := protocol.Logs(items)
|
||||
header := &protocol.EnvelopeHeader{EventID: protocol.GenerateEventID(), SentAt: time.Now(), Sdk: s.sdkInfo}
|
||||
if s.dsn != nil {
|
||||
header.Dsn = s.dsn.String()
|
||||
}
|
||||
envelope := protocol.NewEnvelope(header)
|
||||
item, err := logs.ToEnvelopeItem()
|
||||
if err != nil {
|
||||
debuglog.Printf("error creating log batch envelope item: %v", err)
|
||||
return
|
||||
}
|
||||
envelope.AddItem(item)
|
||||
if err := s.transport.SendEnvelope(envelope); err != nil {
|
||||
debuglog.Printf("error sending envelope: %v", err)
|
||||
}
|
||||
return
|
||||
case ratelimit.CategoryTraceMetric:
|
||||
metrics := protocol.Metrics(items)
|
||||
header := &protocol.EnvelopeHeader{EventID: protocol.GenerateEventID(), SentAt: time.Now(), Sdk: s.sdkInfo}
|
||||
if s.dsn != nil {
|
||||
header.Dsn = s.dsn.String()
|
||||
}
|
||||
envelope := protocol.NewEnvelope(header)
|
||||
item, err := metrics.ToEnvelopeItem()
|
||||
if err != nil {
|
||||
debuglog.Printf("error creating trace metric batch envelope item: %v", err)
|
||||
return
|
||||
}
|
||||
envelope.AddItem(item)
|
||||
if err := s.transport.SendEnvelope(envelope); err != nil {
|
||||
debuglog.Printf("error sending envelope: %v", err)
|
||||
}
|
||||
return
|
||||
default:
|
||||
// if the buffers are properly configured, buffer.PollIfReady should return a single item for every category
|
||||
// other than logs. We still iterate over the items just in case, because we don't want to send broken envelopes.
|
||||
for _, it := range items {
|
||||
convertible, ok := it.(protocol.EnvelopeItemConvertible)
|
||||
if !ok {
|
||||
debuglog.Printf("item does not implement EnvelopeItemConvertible: %T", it)
|
||||
continue
|
||||
}
|
||||
s.sendItem(convertible)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Scheduler) sendItem(item protocol.EnvelopeItemConvertible) {
|
||||
header := &protocol.EnvelopeHeader{
|
||||
EventID: item.GetEventID(),
|
||||
SentAt: time.Now(),
|
||||
Trace: item.GetDynamicSamplingContext(),
|
||||
Sdk: s.sdkInfo,
|
||||
}
|
||||
if header.EventID == "" {
|
||||
header.EventID = protocol.GenerateEventID()
|
||||
}
|
||||
if s.dsn != nil {
|
||||
header.Dsn = s.dsn.String()
|
||||
}
|
||||
envelope := protocol.NewEnvelope(header)
|
||||
envItem, err := item.ToEnvelopeItem()
|
||||
if err != nil {
|
||||
debuglog.Printf("error while converting to envelope item: %v", err)
|
||||
return
|
||||
}
|
||||
envelope.AddItem(envItem)
|
||||
if err := s.transport.SendEnvelope(envelope); err != nil {
|
||||
debuglog.Printf("error sending envelope: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Scheduler) flushBuffers() {
|
||||
for category, buffer := range s.buffers {
|
||||
if !buffer.IsEmpty() {
|
||||
s.processItems(buffer, category, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Scheduler) isRateLimited(category ratelimit.Category) bool {
|
||||
return s.transport.IsRateLimited(category)
|
||||
}
|
||||
42
vendor/github.com/getsentry/sentry-go/internal/telemetry/storage.go
generated
vendored
Normal file
42
vendor/github.com/getsentry/sentry-go/internal/telemetry/storage.go
generated
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
package telemetry
|
||||
|
||||
import (
|
||||
"github.com/getsentry/sentry-go/internal/ratelimit"
|
||||
)
|
||||
|
||||
// Buffer defines the common interface for all buffer implementations.
|
||||
type Buffer[T any] interface {
|
||||
// Core operations
|
||||
Offer(item T) bool
|
||||
Poll() (T, bool)
|
||||
PollBatch(maxItems int) []T
|
||||
PollIfReady() []T
|
||||
Drain() []T
|
||||
Peek() (T, bool)
|
||||
|
||||
// State queries
|
||||
Size() int
|
||||
Capacity() int
|
||||
IsEmpty() bool
|
||||
IsFull() bool
|
||||
Utilization() float64
|
||||
|
||||
// Flush management
|
||||
IsReadyToFlush() bool
|
||||
MarkFlushed()
|
||||
|
||||
// Category/Priority
|
||||
Category() ratelimit.Category
|
||||
Priority() ratelimit.Priority
|
||||
|
||||
// Metrics
|
||||
OfferedCount() int64
|
||||
DroppedCount() int64
|
||||
AcceptedCount() int64
|
||||
DropRate() float64
|
||||
GetMetrics() BufferMetrics
|
||||
|
||||
// Configuration
|
||||
SetDroppedCallback(callback func(item T, reason string))
|
||||
Clear()
|
||||
}
|
||||
7
vendor/github.com/getsentry/sentry-go/internal/telemetry/trace_aware.go
generated
vendored
Normal file
7
vendor/github.com/getsentry/sentry-go/internal/telemetry/trace_aware.go
generated
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
package telemetry
|
||||
|
||||
// TraceAware is implemented by items that can expose a trace ID.
|
||||
// BucketedBuffer uses this to group items by trace.
|
||||
type TraceAware interface {
|
||||
GetTraceID() (string, bool)
|
||||
}
|
||||
15
vendor/github.com/getsentry/sentry-go/internal/traceparser/README.md
generated
vendored
15
vendor/github.com/getsentry/sentry-go/internal/traceparser/README.md
generated
vendored
@@ -1,15 +0,0 @@
|
||||
## Benchmark results
|
||||
|
||||
```
|
||||
goos: windows
|
||||
goarch: amd64
|
||||
pkg: github.com/getsentry/sentry-go/internal/trace
|
||||
cpu: 12th Gen Intel(R) Core(TM) i7-12700K
|
||||
BenchmarkEqualBytes-20 44323621 26.08 ns/op
|
||||
BenchmarkStringEqual-20 60980257 18.27 ns/op
|
||||
BenchmarkEqualPrefix-20 41369181 31.12 ns/op
|
||||
BenchmarkFullParse-20 702012 1507 ns/op 1353.42 MB/s 1024 B/op 6 allocs/op
|
||||
BenchmarkFramesIterator-20 1229971 969.3 ns/op 896 B/op 5 allocs/op
|
||||
BenchmarkFramesReversedIterator-20 1271061 944.5 ns/op 896 B/op 5 allocs/op
|
||||
BenchmarkSplitOnly-20 2250800 534.0 ns/op 3818.23 MB/s 128 B/op 1 allocs/op
|
||||
```
|
||||
217
vendor/github.com/getsentry/sentry-go/internal/traceparser/parser.go
generated
vendored
217
vendor/github.com/getsentry/sentry-go/internal/traceparser/parser.go
generated
vendored
@@ -1,217 +0,0 @@
|
||||
package traceparser
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var blockSeparator = []byte("\n\n")
|
||||
var lineSeparator = []byte("\n")
|
||||
|
||||
// Parses multi-stacktrace text dump produced by runtime.Stack([]byte, all=true).
|
||||
// The parser prioritizes performance but requires the input to be well-formed in order to return correct data.
|
||||
// See https://github.com/golang/go/blob/go1.20.4/src/runtime/mprof.go#L1191
|
||||
func Parse(data []byte) TraceCollection {
|
||||
var it = TraceCollection{}
|
||||
if len(data) > 0 {
|
||||
it.blocks = bytes.Split(data, blockSeparator)
|
||||
}
|
||||
return it
|
||||
}
|
||||
|
||||
type TraceCollection struct {
|
||||
blocks [][]byte
|
||||
}
|
||||
|
||||
func (it TraceCollection) Length() int {
|
||||
return len(it.blocks)
|
||||
}
|
||||
|
||||
// Returns the stacktrace item at the given index.
|
||||
func (it *TraceCollection) Item(i int) Trace {
|
||||
// The first item may have a leading data separator and the last one may have a trailing one.
|
||||
// Note: Trim() doesn't make a copy for single-character cutset under 0x80. It will just slice the original.
|
||||
var data []byte
|
||||
switch {
|
||||
case i == 0:
|
||||
data = bytes.TrimLeft(it.blocks[i], "\n")
|
||||
case i == len(it.blocks)-1:
|
||||
data = bytes.TrimRight(it.blocks[i], "\n")
|
||||
default:
|
||||
data = it.blocks[i]
|
||||
}
|
||||
|
||||
var splitAt = bytes.IndexByte(data, '\n')
|
||||
if splitAt < 0 {
|
||||
return Trace{header: data}
|
||||
}
|
||||
|
||||
return Trace{
|
||||
header: data[:splitAt],
|
||||
data: data[splitAt+1:],
|
||||
}
|
||||
}
|
||||
|
||||
// Trace represents a single stacktrace block, identified by a Goroutine ID and a sequence of Frames.
|
||||
type Trace struct {
|
||||
header []byte
|
||||
data []byte
|
||||
}
|
||||
|
||||
var goroutinePrefix = []byte("goroutine ")
|
||||
|
||||
// GoID parses the Goroutine ID from the header.
|
||||
func (t *Trace) GoID() (id uint64) {
|
||||
if bytes.HasPrefix(t.header, goroutinePrefix) {
|
||||
var line = t.header[len(goroutinePrefix):]
|
||||
var splitAt = bytes.IndexByte(line, ' ')
|
||||
if splitAt >= 0 {
|
||||
id, _ = strconv.ParseUint(string(line[:splitAt]), 10, 64)
|
||||
}
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
// UniqueIdentifier can be used as a map key to identify the trace.
|
||||
func (t *Trace) UniqueIdentifier() []byte {
|
||||
return t.data
|
||||
}
|
||||
|
||||
func (t *Trace) Frames() FrameIterator {
|
||||
var lines = bytes.Split(t.data, lineSeparator)
|
||||
return FrameIterator{lines: lines, i: 0, len: len(lines)}
|
||||
}
|
||||
|
||||
func (t *Trace) FramesReversed() ReverseFrameIterator {
|
||||
var lines = bytes.Split(t.data, lineSeparator)
|
||||
return ReverseFrameIterator{lines: lines, i: len(lines)}
|
||||
}
|
||||
|
||||
const framesElided = "...additional frames elided..."
|
||||
|
||||
// FrameIterator iterates over stack frames.
|
||||
type FrameIterator struct {
|
||||
lines [][]byte
|
||||
i int
|
||||
len int
|
||||
}
|
||||
|
||||
// Next returns the next frame, or nil if there are none.
|
||||
func (it *FrameIterator) Next() Frame {
|
||||
return Frame{it.popLine(), it.popLine()}
|
||||
}
|
||||
|
||||
func (it *FrameIterator) popLine() []byte {
|
||||
switch {
|
||||
case it.i >= it.len:
|
||||
return nil
|
||||
case string(it.lines[it.i]) == framesElided:
|
||||
it.i++
|
||||
return it.popLine()
|
||||
default:
|
||||
it.i++
|
||||
return it.lines[it.i-1]
|
||||
}
|
||||
}
|
||||
|
||||
// HasNext return true if there are values to be read.
|
||||
func (it *FrameIterator) HasNext() bool {
|
||||
return it.i < it.len
|
||||
}
|
||||
|
||||
// LengthUpperBound returns the maximum number of elements this stacks may contain.
|
||||
// The actual number may be lower because of elided frames. As such, the returned value
|
||||
// cannot be used to iterate over the frames but may be used to reserve capacity.
|
||||
func (it *FrameIterator) LengthUpperBound() int {
|
||||
return it.len / 2
|
||||
}
|
||||
|
||||
// ReverseFrameIterator iterates over stack frames in reverse order.
|
||||
type ReverseFrameIterator struct {
|
||||
lines [][]byte
|
||||
i int
|
||||
}
|
||||
|
||||
// Next returns the next frame, or nil if there are none.
|
||||
func (it *ReverseFrameIterator) Next() Frame {
|
||||
var line2 = it.popLine()
|
||||
return Frame{it.popLine(), line2}
|
||||
}
|
||||
|
||||
func (it *ReverseFrameIterator) popLine() []byte {
|
||||
it.i--
|
||||
switch {
|
||||
case it.i < 0:
|
||||
return nil
|
||||
case string(it.lines[it.i]) == framesElided:
|
||||
return it.popLine()
|
||||
default:
|
||||
return it.lines[it.i]
|
||||
}
|
||||
}
|
||||
|
||||
// HasNext return true if there are values to be read.
|
||||
func (it *ReverseFrameIterator) HasNext() bool {
|
||||
return it.i > 1
|
||||
}
|
||||
|
||||
// LengthUpperBound returns the maximum number of elements this stacks may contain.
|
||||
// The actual number may be lower because of elided frames. As such, the returned value
|
||||
// cannot be used to iterate over the frames but may be used to reserve capacity.
|
||||
func (it *ReverseFrameIterator) LengthUpperBound() int {
|
||||
return len(it.lines) / 2
|
||||
}
|
||||
|
||||
type Frame struct {
|
||||
line1 []byte
|
||||
line2 []byte
|
||||
}
|
||||
|
||||
// UniqueIdentifier can be used as a map key to identify the frame.
|
||||
func (f *Frame) UniqueIdentifier() []byte {
|
||||
// line2 contains file path, line number and program-counter offset from the beginning of a function
|
||||
// e.g. C:/Users/name/scoop/apps/go/current/src/testing/testing.go:1906 +0x63a
|
||||
return f.line2
|
||||
}
|
||||
|
||||
var createdByPrefix = []byte("created by ")
|
||||
|
||||
func (f *Frame) Func() []byte {
|
||||
if bytes.HasPrefix(f.line1, createdByPrefix) {
|
||||
// Since go1.21, the line ends with " in goroutine X", saying which goroutine created this one.
|
||||
// We currently don't have use for that so just remove it.
|
||||
var line = f.line1[len(createdByPrefix):]
|
||||
var spaceAt = bytes.IndexByte(line, ' ')
|
||||
if spaceAt < 0 {
|
||||
return line
|
||||
}
|
||||
return line[:spaceAt]
|
||||
}
|
||||
|
||||
var end = bytes.LastIndexByte(f.line1, '(')
|
||||
if end >= 0 {
|
||||
return f.line1[:end]
|
||||
}
|
||||
|
||||
return f.line1
|
||||
}
|
||||
|
||||
func (f *Frame) File() (path []byte, lineNumber int) {
|
||||
var line = f.line2
|
||||
if len(line) > 0 && line[0] == '\t' {
|
||||
line = line[1:]
|
||||
}
|
||||
|
||||
var splitAt = bytes.IndexByte(line, ' ')
|
||||
if splitAt >= 0 {
|
||||
line = line[:splitAt]
|
||||
}
|
||||
|
||||
splitAt = bytes.LastIndexByte(line, ':')
|
||||
if splitAt < 0 {
|
||||
return line, 0
|
||||
}
|
||||
|
||||
lineNumber, _ = strconv.Atoi(string(line[splitAt+1:]))
|
||||
return line[:splitAt], lineNumber
|
||||
}
|
||||
Reference in New Issue
Block a user