fix(go.sum): update ResolveSpec dependency to v1.0.87
This commit is contained in:
+964
@@ -0,0 +1,964 @@
|
||||
package sentry
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/getsentry/sentry-go/internal/debuglog"
|
||||
httpinternal "github.com/getsentry/sentry-go/internal/http"
|
||||
"github.com/getsentry/sentry-go/internal/protocol"
|
||||
"github.com/getsentry/sentry-go/internal/ratelimit"
|
||||
"github.com/getsentry/sentry-go/internal/util"
|
||||
"github.com/getsentry/sentry-go/report"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultBufferSize = 1000
|
||||
defaultTimeout = time.Second * 30
|
||||
defaultClientReportsTick = time.Second * 30
|
||||
)
|
||||
|
||||
// Transport is used by the Client to deliver events to remote server.
|
||||
type Transport interface {
|
||||
Flush(timeout time.Duration) bool
|
||||
FlushWithContext(ctx context.Context) bool
|
||||
Configure(options ClientOptions)
|
||||
SendEvent(event *Event)
|
||||
Close()
|
||||
}
|
||||
|
||||
func getProxyConfig(options ClientOptions) func(*http.Request) (*url.URL, error) {
|
||||
if options.HTTPSProxy != "" {
|
||||
return func(*http.Request) (*url.URL, error) {
|
||||
return url.Parse(options.HTTPSProxy)
|
||||
}
|
||||
}
|
||||
|
||||
if options.HTTPProxy != "" {
|
||||
return func(*http.Request) (*url.URL, error) {
|
||||
return url.Parse(options.HTTPProxy)
|
||||
}
|
||||
}
|
||||
|
||||
return http.ProxyFromEnvironment
|
||||
}
|
||||
|
||||
func getTLSConfig(options ClientOptions) *tls.Config {
|
||||
if options.CaCerts != nil {
|
||||
// #nosec G402 -- We should be using `MinVersion: tls.VersionTLS12`,
|
||||
// but we don't want to break peoples code without the major bump.
|
||||
return &tls.Config{
|
||||
RootCAs: options.CaCerts,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// eventDebugContext returns a panic-safe summary of an event for debug logging.
|
||||
// It only touches scalar fields and len() of collections, which do not trigger
|
||||
// the runtime concurrent-map-access fatal nor invoke user-defined Stringers.
|
||||
func eventDebugContext(event *Event) string {
|
||||
return fmt.Sprintf(
|
||||
"event=%s type=%s level=%s "+
|
||||
"breadcrumbs=%d exceptions=%d tags=%d contexts=%d attachments=%d",
|
||||
event.EventID, event.Type, event.Level,
|
||||
len(event.Breadcrumbs), len(event.Exception),
|
||||
len(event.Tags), len(event.Contexts), len(event.Attachments),
|
||||
)
|
||||
}
|
||||
|
||||
func getRequestBodyFromEvent(event *Event) []byte {
|
||||
body, err := json.Marshal(event)
|
||||
if err != nil {
|
||||
debuglog.Printf("Could not encode event as JSON, skipping delivery. %s: %v", eventDebugContext(event), err)
|
||||
return nil
|
||||
}
|
||||
return body
|
||||
}
|
||||
|
||||
func encodeAttachment(enc *json.Encoder, b io.Writer, attachment *Attachment) error {
|
||||
// Attachment header
|
||||
err := enc.Encode(struct {
|
||||
Type string `json:"type"`
|
||||
Length int `json:"length"`
|
||||
Filename string `json:"filename"`
|
||||
ContentType string `json:"content_type,omitempty"`
|
||||
}{
|
||||
Type: "attachment",
|
||||
Length: len(attachment.Payload),
|
||||
Filename: attachment.Filename,
|
||||
ContentType: attachment.ContentType,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Attachment payload
|
||||
if _, err = b.Write(attachment.Payload); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// "Envelopes should be terminated with a trailing newline."
|
||||
//
|
||||
// [1]: https://develop.sentry.dev/sdk/envelopes/#envelopes
|
||||
if _, err := b.Write([]byte("\n")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeClientReport(enc *json.Encoder, cr *report.ClientReport) error {
|
||||
payload, err := json.Marshal(cr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = encodeEnvelopeItem(enc, string(protocol.EnvelopeItemTypeClientReport), payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeEnvelopeItem(enc *json.Encoder, itemType string, body json.RawMessage) error {
|
||||
// Item header
|
||||
err := enc.Encode(struct {
|
||||
Type string `json:"type"`
|
||||
Length int `json:"length"`
|
||||
}{
|
||||
Type: itemType,
|
||||
Length: len(body),
|
||||
})
|
||||
if err == nil {
|
||||
// payload
|
||||
err = enc.Encode(body)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func encodeEnvelopeLogs(enc *json.Encoder, count int, body json.RawMessage) error {
|
||||
err := enc.Encode(
|
||||
struct {
|
||||
Type string `json:"type"`
|
||||
ItemCount int `json:"item_count"`
|
||||
ContentType string `json:"content_type"`
|
||||
}{
|
||||
Type: logEvent.Type,
|
||||
ItemCount: count,
|
||||
ContentType: logEvent.ContentType,
|
||||
})
|
||||
if err == nil {
|
||||
err = enc.Encode(body)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func encodeEnvelopeMetrics(enc *json.Encoder, count int, body json.RawMessage) error {
|
||||
err := enc.Encode(
|
||||
struct {
|
||||
Type string `json:"type"`
|
||||
ItemCount int `json:"item_count"`
|
||||
ContentType string `json:"content_type"`
|
||||
}{
|
||||
Type: traceMetricEvent.Type,
|
||||
ItemCount: count,
|
||||
ContentType: traceMetricEvent.ContentType,
|
||||
})
|
||||
if err == nil {
|
||||
err = enc.Encode(body)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func recordForEvent(recorder report.ClientReportRecorder, reason report.DiscardReason, event *Event) {
|
||||
category := event.toCategory()
|
||||
switch event.Type {
|
||||
case transactionType:
|
||||
recorder.RecordOne(reason, category)
|
||||
if count := event.GetSpanCount(); count > 0 {
|
||||
recorder.Record(reason, ratelimit.CategorySpan, int64(count))
|
||||
}
|
||||
case logEvent.Type:
|
||||
if count := len(event.Logs); count > 0 {
|
||||
recorder.Record(reason, ratelimit.CategoryLog, int64(count))
|
||||
}
|
||||
if size := event.GetLogByteSize(); size > 0 {
|
||||
recorder.Record(reason, ratelimit.CategoryLogByte, int64(size))
|
||||
}
|
||||
default:
|
||||
recorder.RecordOne(reason, category)
|
||||
}
|
||||
}
|
||||
|
||||
func recordForBatchItem(recorder report.ClientReportRecorder, reason report.DiscardReason, item *batchItem) {
|
||||
switch {
|
||||
case item.logItemCount > 0:
|
||||
recorder.Record(reason, ratelimit.CategoryLog, int64(item.logItemCount))
|
||||
if item.logByteSize > 0 {
|
||||
recorder.Record(reason, ratelimit.CategoryLogByte, int64(item.logByteSize))
|
||||
}
|
||||
default:
|
||||
recorder.RecordOne(reason, item.category)
|
||||
if item.spanCount > 0 {
|
||||
recorder.Record(reason, ratelimit.CategorySpan, int64(item.spanCount))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// envelopeHeader represents the header of a Sentry envelope.
|
||||
type envelopeHeader struct {
|
||||
EventID EventID `json:"event_id,omitempty"`
|
||||
SentAt time.Time `json:"sent_at"`
|
||||
Dsn *Dsn `json:"dsn,omitempty"`
|
||||
Sdk map[string]string `json:"sdk,omitempty"`
|
||||
Trace map[string]string `json:"trace,omitempty"`
|
||||
}
|
||||
|
||||
func encodeEnvelopeHeader(enc *json.Encoder, header *envelopeHeader) error {
|
||||
return enc.Encode(header)
|
||||
}
|
||||
|
||||
func envelopeFromBody(event *Event, dsn *Dsn, sentAt time.Time, body json.RawMessage) (*bytes.Buffer, error) {
|
||||
var b bytes.Buffer
|
||||
enc := json.NewEncoder(&b)
|
||||
|
||||
// Construct the trace envelope header
|
||||
var trace = map[string]string{}
|
||||
if dsc := event.sdkMetaData.dsc; dsc.HasEntries() {
|
||||
for k, v := range dsc.Entries {
|
||||
trace[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// Envelope header
|
||||
err := encodeEnvelopeHeader(enc, &envelopeHeader{
|
||||
EventID: event.EventID,
|
||||
SentAt: sentAt,
|
||||
Trace: trace,
|
||||
Dsn: dsn,
|
||||
Sdk: map[string]string{
|
||||
"name": event.Sdk.Name,
|
||||
"version": event.Sdk.Version,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch event.Type {
|
||||
case transactionType, checkInType:
|
||||
err = encodeEnvelopeItem(enc, event.Type, body)
|
||||
case logEvent.Type:
|
||||
err = encodeEnvelopeLogs(enc, len(event.Logs), body)
|
||||
case traceMetricEvent.Type:
|
||||
err = encodeEnvelopeMetrics(enc, len(event.Metrics), body)
|
||||
default:
|
||||
err = encodeEnvelopeItem(enc, eventType, body)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Attachments
|
||||
for _, attachment := range event.Attachments {
|
||||
if err := encodeAttachment(enc, &b, attachment); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &b, nil
|
||||
}
|
||||
|
||||
// getRequestFromEnvelope creates an HTTP request from a pre-built envelope.
|
||||
// sdkName and sdkVersion are used for User-Agent and authentication headers.
|
||||
func getRequestFromEnvelope(ctx context.Context, dsn *Dsn, envelope *bytes.Buffer, sdkName, sdkVersion string) (*http.Request, error) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
request, err := http.NewRequestWithContext(
|
||||
ctx,
|
||||
http.MethodPost,
|
||||
dsn.GetAPIURL().String(),
|
||||
envelope,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
request.Header.Set("User-Agent", fmt.Sprintf("%s/%s", sdkName, sdkVersion))
|
||||
request.Header.Set("Content-Type", "application/x-sentry-envelope")
|
||||
|
||||
auth := fmt.Sprintf("Sentry sentry_version=%s, "+
|
||||
"sentry_client=%s/%s, sentry_key=%s", apiVersion, sdkName, sdkVersion, dsn.GetPublicKey())
|
||||
|
||||
// The key sentry_secret is effectively deprecated and no longer needs to be set.
|
||||
// However, since it was required in older self-hosted versions,
|
||||
// it should still be passed through to Sentry if set.
|
||||
if dsn.GetSecretKey() != "" {
|
||||
auth = fmt.Sprintf("%s, sentry_secret=%s", auth, dsn.GetSecretKey())
|
||||
}
|
||||
|
||||
request.Header.Set("X-Sentry-Auth", auth)
|
||||
|
||||
return request, nil
|
||||
}
|
||||
|
||||
// ================================
|
||||
// HTTPTransport
|
||||
// ================================
|
||||
|
||||
// A batch groups items that are processed sequentially.
|
||||
type batch struct {
|
||||
items chan batchItem
|
||||
started chan struct{} // closed to signal items started to be worked on
|
||||
done chan struct{} // closed to signal completion of all items
|
||||
}
|
||||
|
||||
type batchItem struct {
|
||||
ctx context.Context
|
||||
envelope *bytes.Buffer
|
||||
sdkName string
|
||||
sdkVersion string
|
||||
category ratelimit.Category
|
||||
eventIdentifier string
|
||||
spanCount int
|
||||
logItemCount int
|
||||
logByteSize int
|
||||
}
|
||||
|
||||
// HTTPTransport is the default, non-blocking, implementation of Transport.
|
||||
//
|
||||
// Clients using this transport will enqueue requests in a buffer and return to
|
||||
// the caller before any network communication has happened. Requests are sent
|
||||
// to Sentry sequentially from a background goroutine.
|
||||
type HTTPTransport struct {
|
||||
dsn *Dsn
|
||||
client *http.Client
|
||||
transport http.RoundTripper
|
||||
recorder report.ClientReportRecorder
|
||||
provider report.ClientReportProvider
|
||||
|
||||
// buffer is a channel of batches. Calling Flush terminates work on the
|
||||
// current in-flight items and starts a new batch for subsequent events.
|
||||
buffer chan batch
|
||||
|
||||
startOnce sync.Once
|
||||
closeOnce sync.Once
|
||||
|
||||
// Size of the transport buffer. Defaults to 30.
|
||||
BufferSize int
|
||||
// HTTP Client request timeout. Defaults to 30 seconds.
|
||||
Timeout time.Duration
|
||||
|
||||
mu sync.RWMutex
|
||||
limits ratelimit.Map
|
||||
|
||||
// receiving signal will terminate worker.
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// NewHTTPTransport returns a new pre-configured instance of HTTPTransport.
|
||||
func NewHTTPTransport() *HTTPTransport {
|
||||
transport := HTTPTransport{
|
||||
BufferSize: defaultBufferSize,
|
||||
Timeout: defaultTimeout,
|
||||
done: make(chan struct{}),
|
||||
limits: make(ratelimit.Map),
|
||||
}
|
||||
return &transport
|
||||
}
|
||||
|
||||
// Configure is called by the Client itself, providing its own ClientOptions.
|
||||
func (t *HTTPTransport) Configure(options ClientOptions) {
|
||||
dsn, err := NewDsn(options.Dsn)
|
||||
if err != nil {
|
||||
debuglog.Printf("%v\n", err)
|
||||
return
|
||||
}
|
||||
t.dsn = dsn
|
||||
|
||||
if t.recorder == nil {
|
||||
t.recorder = report.NoopRecorder()
|
||||
}
|
||||
if t.provider == nil {
|
||||
t.provider = report.NoopProvider()
|
||||
}
|
||||
|
||||
// A buffered channel with capacity 1 works like a mutex, ensuring only one
|
||||
// goroutine can access the current batch at a given time. Access is
|
||||
// synchronized by reading from and writing to the channel.
|
||||
t.buffer = make(chan batch, 1)
|
||||
t.buffer <- batch{
|
||||
items: make(chan batchItem, t.BufferSize),
|
||||
started: make(chan struct{}),
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
|
||||
if options.HTTPTransport != nil {
|
||||
t.transport = options.HTTPTransport
|
||||
} else {
|
||||
t.transport = &http.Transport{
|
||||
Proxy: getProxyConfig(options),
|
||||
TLSClientConfig: getTLSConfig(options),
|
||||
}
|
||||
}
|
||||
|
||||
if options.HTTPClient != nil {
|
||||
t.client = options.HTTPClient
|
||||
} else {
|
||||
t.client = &http.Client{
|
||||
Transport: t.transport,
|
||||
Timeout: t.Timeout,
|
||||
}
|
||||
}
|
||||
|
||||
t.startOnce.Do(func() {
|
||||
go t.worker()
|
||||
})
|
||||
}
|
||||
|
||||
// SendEvent assembles a new packet out of Event and sends it to the remote server.
|
||||
func (t *HTTPTransport) SendEvent(event *Event) {
|
||||
t.SendEventWithContext(context.Background(), event)
|
||||
}
|
||||
|
||||
// SendEventWithContext assembles a new packet out of Event and sends it to the remote server.
|
||||
func (t *HTTPTransport) SendEventWithContext(ctx context.Context, event *Event) {
|
||||
if t.dsn == nil {
|
||||
return
|
||||
}
|
||||
|
||||
category := event.toCategory()
|
||||
|
||||
if t.disabled(category) {
|
||||
recordForEvent(t.recorder, report.ReasonRateLimitBackoff, event)
|
||||
return
|
||||
}
|
||||
|
||||
body := getRequestBodyFromEvent(event)
|
||||
if body == nil {
|
||||
recordForEvent(t.recorder, report.ReasonInternalError, event)
|
||||
return
|
||||
}
|
||||
envelope, err := envelopeFromBody(event, t.dsn, time.Now(), body)
|
||||
if err != nil {
|
||||
debuglog.Printf("Failed to build envelope, skipping delivery. %s: %v", eventDebugContext(event), err)
|
||||
recordForEvent(t.recorder, report.ReasonInternalError, event)
|
||||
return
|
||||
}
|
||||
|
||||
// <-t.buffer is equivalent to acquiring a lock to access the current batch.
|
||||
// A few lines below, t.buffer <- b releases the lock.
|
||||
//
|
||||
// The lock must be held during the select block below to guarantee that
|
||||
// b.items is not closed while trying to send to it. Remember that sending
|
||||
// on a closed channel panics.
|
||||
//
|
||||
// Note that the select block takes a bounded amount of CPU time because of
|
||||
// the default case that is executed if sending on b.items would block. That
|
||||
// is, the event is dropped if it cannot be sent immediately to the b.items
|
||||
// channel (used as a queue).
|
||||
b := <-t.buffer
|
||||
|
||||
identifier := eventIdentifier(event)
|
||||
|
||||
select {
|
||||
case b.items <- batchItem{
|
||||
ctx: ctx,
|
||||
envelope: envelope,
|
||||
sdkName: event.Sdk.Name,
|
||||
sdkVersion: event.Sdk.Version,
|
||||
category: category,
|
||||
eventIdentifier: identifier,
|
||||
spanCount: event.GetSpanCount(),
|
||||
logItemCount: len(event.Logs),
|
||||
logByteSize: event.GetLogByteSize(),
|
||||
}:
|
||||
debuglog.Printf(
|
||||
"Sending %s to %s project: %s",
|
||||
identifier,
|
||||
t.dsn.GetHost(),
|
||||
t.dsn.GetProjectID(),
|
||||
)
|
||||
default:
|
||||
debuglog.Printf("Event dropped due to transport buffer being full. %s", eventDebugContext(event))
|
||||
recordForEvent(t.recorder, report.ReasonQueueOverflow, event)
|
||||
}
|
||||
|
||||
t.buffer <- b
|
||||
}
|
||||
|
||||
// Flush waits until any buffered events are sent to the Sentry server, blocking
|
||||
// for at most the given timeout. It returns false if the timeout was reached.
|
||||
// In that case, some events may not have been sent.
|
||||
//
|
||||
// Flush should be called before terminating the program to avoid
|
||||
// unintentionally dropping events.
|
||||
//
|
||||
// Do not call Flush indiscriminately after every call to SendEvent. Instead, to
|
||||
// have the SDK send events over the network synchronously, configure it to use
|
||||
// the HTTPSyncTransport in the call to Init.
|
||||
func (t *HTTPTransport) Flush(timeout time.Duration) bool {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
return t.FlushWithContext(ctx)
|
||||
}
|
||||
|
||||
// FlushWithContext works like Flush, but it accepts a context.Context instead of a timeout.
|
||||
func (t *HTTPTransport) FlushWithContext(ctx context.Context) bool {
|
||||
return t.flushInternal(ctx.Done())
|
||||
}
|
||||
|
||||
func (t *HTTPTransport) flushInternal(timeout <-chan struct{}) bool {
|
||||
// Wait until processing the current batch has started or the timeout.
|
||||
//
|
||||
// We must wait until the worker has seen the current batch, because it is
|
||||
// the only way b.done will be closed. If we do not wait, there is a
|
||||
// possible execution flow in which b.done is never closed, and the only way
|
||||
// out of Flush would be waiting for the timeout, which is undesired.
|
||||
var b batch
|
||||
|
||||
for {
|
||||
select {
|
||||
case b = <-t.buffer:
|
||||
select {
|
||||
case <-b.started:
|
||||
goto started
|
||||
default:
|
||||
t.buffer <- b
|
||||
}
|
||||
case <-timeout:
|
||||
goto fail
|
||||
}
|
||||
}
|
||||
|
||||
started:
|
||||
// Signal that there won't be any more items in this batch, so that the
|
||||
// worker inner loop can end.
|
||||
close(b.items)
|
||||
// Start a new batch for subsequent events.
|
||||
t.buffer <- batch{
|
||||
items: make(chan batchItem, t.BufferSize),
|
||||
started: make(chan struct{}),
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
|
||||
// Wait until the current batch is done or the timeout.
|
||||
select {
|
||||
case <-b.done:
|
||||
debuglog.Println("Buffer flushed successfully.")
|
||||
return true
|
||||
case <-timeout:
|
||||
goto fail
|
||||
}
|
||||
|
||||
fail:
|
||||
debuglog.Println("Buffer flushing was canceled or timed out.")
|
||||
return false
|
||||
}
|
||||
|
||||
// Close will terminate events sending loop.
|
||||
// It useful to prevent goroutines leak in case of multiple HTTPTransport instances initiated.
|
||||
//
|
||||
// Close should be called after Flush and before terminating the program
|
||||
// otherwise some events may be lost.
|
||||
func (t *HTTPTransport) Close() {
|
||||
t.closeOnce.Do(func() {
|
||||
close(t.done)
|
||||
})
|
||||
}
|
||||
|
||||
func (t *HTTPTransport) worker() {
|
||||
crTicker := time.NewTicker(defaultClientReportsTick)
|
||||
defer crTicker.Stop()
|
||||
for b := range t.buffer {
|
||||
// Signal that processing of the current batch has started.
|
||||
close(b.started)
|
||||
|
||||
// Return the batch to the buffer so that other goroutines can use it.
|
||||
// Equivalent to releasing a lock.
|
||||
t.buffer <- b
|
||||
|
||||
// Process all batch items.
|
||||
loop:
|
||||
for {
|
||||
select {
|
||||
case <-t.done:
|
||||
return
|
||||
case <-crTicker.C:
|
||||
r := t.provider.TakeReport()
|
||||
if r != nil {
|
||||
var buf bytes.Buffer
|
||||
enc := json.NewEncoder(&buf)
|
||||
if err := encodeEnvelopeHeader(enc, &envelopeHeader{
|
||||
SentAt: time.Now(),
|
||||
Dsn: t.dsn,
|
||||
Sdk: map[string]string{
|
||||
"name": sdkIdentifier,
|
||||
"version": SDKVersion,
|
||||
},
|
||||
}); err != nil {
|
||||
continue
|
||||
}
|
||||
if err := encodeClientReport(enc, r); err != nil {
|
||||
continue
|
||||
}
|
||||
req, err := getRequestFromEnvelope(context.Background(), t.dsn, &buf, sdkIdentifier, SDKVersion)
|
||||
if err != nil {
|
||||
debuglog.Printf("There was an issue when creating the request: %v", err)
|
||||
continue
|
||||
}
|
||||
result, err := util.DoSendRequest(t.client, req, "client report")
|
||||
if err != nil {
|
||||
debuglog.Printf("There was an issue with sending an event: %v", err)
|
||||
continue
|
||||
}
|
||||
t.mu.Lock()
|
||||
t.limits.Merge(result.Limits)
|
||||
t.mu.Unlock()
|
||||
}
|
||||
case item, open := <-b.items:
|
||||
if !open {
|
||||
break loop
|
||||
}
|
||||
if t.disabled(item.category) {
|
||||
recordForBatchItem(t.recorder, report.ReasonRateLimitBackoff, &item)
|
||||
continue
|
||||
}
|
||||
|
||||
// Attach accumulated client report inside the worker to avoid background queue overflows.
|
||||
t.attachClientReport(item.envelope)
|
||||
request, err := getRequestFromEnvelope(item.ctx, t.dsn, item.envelope, item.sdkName, item.sdkVersion)
|
||||
if err != nil {
|
||||
debuglog.Printf("There was an issue when creating the request: %v", err)
|
||||
recordForBatchItem(t.recorder, report.ReasonInternalError, &item)
|
||||
continue
|
||||
}
|
||||
|
||||
result, err := util.DoSendRequest(t.client, request, item.eventIdentifier)
|
||||
if err != nil {
|
||||
debuglog.Printf("There was an issue with sending an event: %v", err)
|
||||
recordForBatchItem(t.recorder, report.ReasonNetworkError, &item)
|
||||
continue
|
||||
}
|
||||
if result.IsSendError() {
|
||||
recordForBatchItem(t.recorder, report.ReasonSendError, &item)
|
||||
}
|
||||
|
||||
t.mu.Lock()
|
||||
t.limits.Merge(result.Limits)
|
||||
t.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// Signal that processing of the batch is done.
|
||||
close(b.done)
|
||||
}
|
||||
}
|
||||
|
||||
// attachClientReport takes any pending client report from the provider and
|
||||
// appends it to the envelope buffer.
|
||||
func (t *HTTPTransport) attachClientReport(buf *bytes.Buffer) {
|
||||
r := t.provider.TakeReport()
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
enc := json.NewEncoder(buf)
|
||||
if err := encodeClientReport(enc, r); err != nil {
|
||||
debuglog.Printf("Failed to encode client report: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *HTTPTransport) disabled(c ratelimit.Category) bool {
|
||||
t.mu.RLock()
|
||||
defer t.mu.RUnlock()
|
||||
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
|
||||
}
|
||||
|
||||
// ================================
|
||||
// HTTPSyncTransport
|
||||
// ================================
|
||||
|
||||
// HTTPSyncTransport 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 HTTPTransport.
|
||||
type HTTPSyncTransport struct {
|
||||
dsn *Dsn
|
||||
client *http.Client
|
||||
transport http.RoundTripper
|
||||
recorder report.ClientReportRecorder
|
||||
provider report.ClientReportProvider
|
||||
|
||||
mu sync.Mutex
|
||||
limits ratelimit.Map
|
||||
|
||||
// HTTP Client request timeout. Defaults to 30 seconds.
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
// NewHTTPSyncTransport returns a new pre-configured instance of HTTPSyncTransport.
|
||||
func NewHTTPSyncTransport() *HTTPSyncTransport {
|
||||
transport := HTTPSyncTransport{
|
||||
Timeout: defaultTimeout,
|
||||
limits: make(ratelimit.Map),
|
||||
}
|
||||
|
||||
return &transport
|
||||
}
|
||||
|
||||
// Configure is called by the Client itself, providing its own ClientOptions.
|
||||
func (t *HTTPSyncTransport) Configure(options ClientOptions) {
|
||||
dsn, err := NewDsn(options.Dsn)
|
||||
if err != nil {
|
||||
debuglog.Printf("%v\n", err)
|
||||
return
|
||||
}
|
||||
t.dsn = dsn
|
||||
|
||||
if t.recorder == nil {
|
||||
t.recorder = report.NoopRecorder()
|
||||
}
|
||||
if t.provider == nil {
|
||||
t.provider = report.NoopProvider()
|
||||
}
|
||||
|
||||
if options.HTTPTransport != nil {
|
||||
t.transport = options.HTTPTransport
|
||||
} else {
|
||||
t.transport = &http.Transport{
|
||||
Proxy: getProxyConfig(options),
|
||||
TLSClientConfig: getTLSConfig(options),
|
||||
}
|
||||
}
|
||||
|
||||
if options.HTTPClient != nil {
|
||||
t.client = options.HTTPClient
|
||||
} else {
|
||||
t.client = &http.Client{
|
||||
Transport: t.transport,
|
||||
Timeout: t.Timeout,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SendEvent assembles a new packet out of Event and sends it to the remote server.
|
||||
func (t *HTTPSyncTransport) SendEvent(event *Event) {
|
||||
t.SendEventWithContext(context.Background(), event)
|
||||
}
|
||||
|
||||
func (t *HTTPSyncTransport) Close() {}
|
||||
|
||||
// SendEventWithContext assembles a new packet out of Event and sends it to the remote server.
|
||||
func (t *HTTPSyncTransport) SendEventWithContext(ctx context.Context, event *Event) {
|
||||
if t.dsn == nil {
|
||||
return
|
||||
}
|
||||
|
||||
category := event.toCategory()
|
||||
if t.disabled(category) {
|
||||
recordForEvent(t.recorder, report.ReasonRateLimitBackoff, event)
|
||||
return
|
||||
}
|
||||
|
||||
body := getRequestBodyFromEvent(event)
|
||||
if body == nil {
|
||||
recordForEvent(t.recorder, report.ReasonInternalError, event)
|
||||
return
|
||||
}
|
||||
|
||||
envelope, err := envelopeFromBody(event, t.dsn, time.Now(), body)
|
||||
if err != nil {
|
||||
debuglog.Printf("Failed to build envelope, skipping delivery. %s: %v", eventDebugContext(event), err)
|
||||
recordForEvent(t.recorder, report.ReasonInternalError, event)
|
||||
return
|
||||
}
|
||||
|
||||
if r := t.provider.TakeReport(); r != nil {
|
||||
enc := json.NewEncoder(envelope)
|
||||
if err := encodeClientReport(enc, r); err != nil {
|
||||
debuglog.Printf("Failed to encode client report: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
request, err := getRequestFromEnvelope(ctx, t.dsn, envelope, event.Sdk.Name, event.Sdk.Version)
|
||||
if err != nil {
|
||||
recordForEvent(t.recorder, report.ReasonInternalError, event)
|
||||
return
|
||||
}
|
||||
|
||||
identifier := eventIdentifier(event)
|
||||
debuglog.Printf(
|
||||
"Sending %s to %s project: %s",
|
||||
identifier,
|
||||
t.dsn.GetHost(),
|
||||
t.dsn.GetProjectID(),
|
||||
)
|
||||
|
||||
result, err := util.DoSendRequest(t.client, request, identifier)
|
||||
if err != nil {
|
||||
debuglog.Printf("There was an issue with sending an event: %v", err)
|
||||
recordForEvent(t.recorder, report.ReasonNetworkError, event)
|
||||
return
|
||||
}
|
||||
if result.IsSendError() {
|
||||
recordForEvent(t.recorder, report.ReasonSendError, event)
|
||||
}
|
||||
|
||||
t.mu.Lock()
|
||||
t.limits.Merge(result.Limits)
|
||||
t.mu.Unlock()
|
||||
}
|
||||
|
||||
// Flush is a no-op for HTTPSyncTransport. It always returns true immediately.
|
||||
func (t *HTTPSyncTransport) Flush(_ time.Duration) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// FlushWithContext is a no-op for HTTPSyncTransport. It always returns true immediately.
|
||||
func (t *HTTPSyncTransport) FlushWithContext(_ context.Context) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (t *HTTPSyncTransport) 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
|
||||
}
|
||||
|
||||
// ================================
|
||||
// noopTransport
|
||||
// ================================
|
||||
|
||||
// noopTransport is an implementation of Transport interface which drops all the events.
|
||||
// Only used internally when an empty DSN is provided, which effectively disables the SDK.
|
||||
type noopTransport struct{}
|
||||
|
||||
var _ Transport = noopTransport{}
|
||||
|
||||
func (noopTransport) Configure(ClientOptions) {
|
||||
debuglog.Println("Sentry client initialized with an empty DSN. Using noopTransport. No events will be delivered.")
|
||||
}
|
||||
|
||||
func (noopTransport) SendEvent(*Event) {
|
||||
debuglog.Println("Event dropped due to noopTransport usage.")
|
||||
}
|
||||
|
||||
func (noopTransport) Flush(time.Duration) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (noopTransport) FlushWithContext(context.Context) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (noopTransport) Close() {}
|
||||
|
||||
// ================================
|
||||
// Internal Transport Adapters
|
||||
// ================================
|
||||
|
||||
// newInternalAsyncTransport creates a new AsyncTransport from internal/http
|
||||
// wrapped to satisfy the Transport interface.
|
||||
//
|
||||
// This is not yet exposed in the public API and is for internal experimentation.
|
||||
func newInternalAsyncTransport() Transport {
|
||||
return &internalAsyncTransportAdapter{}
|
||||
}
|
||||
|
||||
// internalAsyncTransportAdapter wraps the internal AsyncTransport to implement
|
||||
// the root-level Transport interface.
|
||||
type internalAsyncTransportAdapter struct {
|
||||
transport protocol.TelemetryTransport
|
||||
dsn *protocol.Dsn
|
||||
recorder report.ClientReportRecorder
|
||||
provider report.ClientReportProvider
|
||||
}
|
||||
|
||||
func (a *internalAsyncTransportAdapter) Configure(options ClientOptions) {
|
||||
transportOptions := httpinternal.TransportOptions{
|
||||
Dsn: options.Dsn,
|
||||
HTTPClient: options.HTTPClient,
|
||||
HTTPTransport: options.HTTPTransport,
|
||||
HTTPProxy: options.HTTPProxy,
|
||||
HTTPSProxy: options.HTTPSProxy,
|
||||
CaCerts: options.CaCerts,
|
||||
Recorder: a.recorder,
|
||||
Provider: a.provider,
|
||||
SdkInfo: func() *protocol.SdkInfo {
|
||||
return &protocol.SdkInfo{
|
||||
Name: sdkIdentifier,
|
||||
Version: SDKVersion,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
a.transport = httpinternal.NewAsyncTransport(transportOptions)
|
||||
|
||||
if options.Dsn != "" {
|
||||
dsn, err := protocol.NewDsn(options.Dsn)
|
||||
if err != nil {
|
||||
debuglog.Printf("Failed to parse DSN in adapter: %v\n", err)
|
||||
} else {
|
||||
a.dsn = dsn
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *internalAsyncTransportAdapter) SendEvent(event *Event) {
|
||||
header := &protocol.EnvelopeHeader{EventID: string(event.EventID), SentAt: time.Now(), Dsn: a.dsn, Sdk: &protocol.SdkInfo{Name: event.Sdk.Name, Version: event.Sdk.Version}}
|
||||
if header.EventID == "" {
|
||||
header.EventID = protocol.GenerateEventID()
|
||||
}
|
||||
envelope, err := event.ToEnvelope(header)
|
||||
if err != nil {
|
||||
debuglog.Printf("Failed to convert event to envelope, skipping delivery. %s: %v", eventDebugContext(event), err)
|
||||
if a.recorder != nil {
|
||||
recordForEvent(a.recorder, report.ReasonInternalError, event)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err := a.transport.SendEnvelope(envelope); err != nil {
|
||||
debuglog.Printf("Error sending envelope: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *internalAsyncTransportAdapter) Flush(timeout time.Duration) bool {
|
||||
return a.transport.Flush(timeout)
|
||||
}
|
||||
|
||||
func (a *internalAsyncTransportAdapter) FlushWithContext(ctx context.Context) bool {
|
||||
return a.transport.FlushWithContext(ctx)
|
||||
}
|
||||
|
||||
func (a *internalAsyncTransportAdapter) Close() {
|
||||
a.transport.Close()
|
||||
}
|
||||
Reference in New Issue
Block a user