gбновление зависимостей, рефакторинг кода, чтобы он выглядел как гошный проект

This commit is contained in:
eleksir
2025-04-06 20:22:16 +03:00
parent 00257b8e6c
commit cfb98fc0b3
389 changed files with 18649 additions and 9105 deletions

View File

@@ -8,6 +8,33 @@ targets:
- name: github
tagPrefix: otel/v
tagOnly: true
- name: github
tagPrefix: echo/v
tagOnly: true
- name: github
tagPrefix: fasthttp/v
tagOnly: true
- name: github
tagPrefix: fiber/v
tagOnly: true
- name: github
tagPrefix: gin/v
tagOnly: true
- name: github
tagPrefix: iris/v
tagOnly: true
- name: github
tagPrefix: negroni/v
tagOnly: true
- name: github
tagPrefix: logrus/v
tagOnly: true
- name: github
tagPrefix: slog/v
tagOnly: true
- name: github
tagPrefix: zerolog/v
tagOnly: true
- name: registry
sdks:
github:getsentry/sentry-go:

View File

@@ -1,5 +1,143 @@
# Changelog
## 0.31.1
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.31.1.
### Bug Fixes
- Correct wrong module name for `sentry-go/logrus` ([#950](https://github.com/getsentry/sentry-go/pull/950))
## 0.31.0
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.31.0.
### Breaking Changes
- Remove support for metrics. Read more about the end of the Metrics beta [here](https://sentry.zendesk.com/hc/en-us/articles/26369339769883-Metrics-Beta-Ended-on-October-7th). ([#914](https://github.com/getsentry/sentry-go/pull/914))
- Remove support for profiling. ([#915](https://github.com/getsentry/sentry-go/pull/915))
- Remove `Segment` field from the `User` struct. This field is no longer used in the Sentry product. ([#928](https://github.com/getsentry/sentry-go/pull/928))
- Every integration is now a separate module, reducing the binary size and number of dependencies. Once you update `sentry-go` to latest version, you'll need to `go get` the integration you want to use. For example, if you want to use the `echo` integration, you'll need to run `go get github.com/getsentry/sentry-go/echo` ([#919](github.com/getsentry/sentry-go/pull/919)).
### Features
Add the ability to override `hub` in `context` for integrations that use custom context. ([#931](https://github.com/getsentry/sentry-go/pull/931))
- Add `HubProvider` Hook for `sentrylogrus`, enabling dynamic Sentry hub allocation for each log entry or goroutine. ([#936](https://github.com/getsentry/sentry-go/pull/936))
This change enhances compatibility with Sentry's recommendation of using separate hubs per goroutine. To ensure a separate Sentry hub for each goroutine, configure the `HubProvider` like this:
```go
hook, err := sentrylogrus.New(nil, sentry.ClientOptions{})
if err != nil {
log.Fatalf("Failed to initialize Sentry hook: %v", err)
}
// Set a custom HubProvider to generate a new hub for each goroutine or log entry
hook.SetHubProvider(func() *sentry.Hub {
client, _ := sentry.NewClient(sentry.ClientOptions{})
return sentry.NewHub(client, sentry.NewScope())
})
logrus.AddHook(hook)
```
### Bug Fixes
- Add support for closing worker goroutines started by the `HTTPTranport` to prevent goroutine leaks. ([#894](https://github.com/getsentry/sentry-go/pull/894))
```go
client, _ := sentry.NewClient()
defer client.Close()
```
Worker can be also closed by calling `Close()` method on the `HTTPTransport` instance. `Close` should be called after `Flush` and before terminating the program otherwise some events may be lost.
```go
transport := sentry.NewHTTPTransport()
defer transport.Close()
```
### Misc
- Bump [gin-gonic/gin](https://github.com/gin-gonic/gin) to v1.9.1. ([#946](https://github.com/getsentry/sentry-go/pull/946))
## 0.30.0
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.30.0.
### Features
- Add `sentryzerolog` integration ([#857](https://github.com/getsentry/sentry-go/pull/857))
- Add `sentryslog` integration ([#865](https://github.com/getsentry/sentry-go/pull/865))
- Always set Mechanism Type to generic ([#896](https://github.com/getsentry/sentry-go/pull/897))
### Bug Fixes
- Prevent panic in `fasthttp` and `fiber` integration in case a malformed URL has to be parsed ([#912](https://github.com/getsentry/sentry-go/pull/912))
### Misc
Drop support for Go 1.18, 1.19 and 1.20. The currently supported Go versions are the last 3 stable releases: 1.23, 1.22 and 1.21.
## 0.29.1
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.29.1.
### Bug Fixes
- Correlate errors to the current trace ([#886](https://github.com/getsentry/sentry-go/pull/886))
- Set the trace context when the transaction finishes ([#888](https://github.com/getsentry/sentry-go/pull/888))
### Misc
- Update the `sentrynegroni` integration to use the latest (v3.1.1) version of Negroni ([#885](https://github.com/getsentry/sentry-go/pull/885))
## 0.29.0
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.29.0.
### Breaking Changes
- Remove the `sentrymartini` integration ([#861](https://github.com/getsentry/sentry-go/pull/861))
- The `WrapResponseWriter` has been moved from the `sentryhttp` package to the `internal/httputils` package. If you've imported it previosuly, you'll need to copy the implementation in your project. ([#871](https://github.com/getsentry/sentry-go/pull/871))
### Features
- Add new convenience methods to continue a trace and propagate tracing headers for error-only use cases. ([#862](https://github.com/getsentry/sentry-go/pull/862))
If you are not using one of our integrations, you can manually continue an incoming trace by using `sentry.ContinueTrace()` by providing the `sentry-trace` and `baggage` header received from a downstream SDK.
```go
hub := sentry.CurrentHub()
sentry.ContinueTrace(hub, r.Header.Get(sentry.SentryTraceHeader), r.Header.Get(sentry.SentryBaggageHeader)),
```
You can use `hub.GetTraceparent()` and `hub.GetBaggage()` to fetch the necessary header values for outgoing HTTP requests.
```go
hub := sentry.GetHubFromContext(ctx)
req, _ := http.NewRequest("GET", "http://localhost:3000", nil)
req.Header.Add(sentry.SentryTraceHeader, hub.GetTraceparent())
req.Header.Add(sentry.SentryBaggageHeader, hub.GetBaggage())
```
### Bug Fixes
- Initialize `HTTPTransport.limit` if `nil` ([#844](https://github.com/getsentry/sentry-go/pull/844))
- Fix `sentry.StartTransaction()` returning a transaction with an outdated context on existing transactions ([#854](https://github.com/getsentry/sentry-go/pull/854))
- Treat `Proxy-Authorization` as a sensitive header ([#859](https://github.com/getsentry/sentry-go/pull/859))
- Add support for the `http.Hijacker` interface to the `sentrynegroni` package ([#871](https://github.com/getsentry/sentry-go/pull/871))
- Go version >= 1.23: Use value from `http.Request.Pattern` for HTTP transaction names when using `sentryhttp` & `sentrynegroni` ([#875](https://github.com/getsentry/sentry-go/pull/875))
- Go version >= 1.21: Fix closure functions name grouping ([#877](https://github.com/getsentry/sentry-go/pull/877))
### Misc
- Collect `span` origins ([#849](https://github.com/getsentry/sentry-go/pull/849))
## 0.28.1
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.28.1.

View File

@@ -58,9 +58,8 @@ test-coverage: $(COVERAGE_REPORT_DIR) clean-report-dir ## Test with coverage en
mod-tidy: ## Check go.mod tidiness
set -e ; \
for dir in $(ALL_GO_MOD_DIRS); do \
cd "$${dir}"; \
echo ">>> Running 'go mod tidy' for module: $${dir}"; \
go mod tidy -go=1.18 -compat=1.18; \
(cd "$${dir}" && go mod tidy -go=1.21 -compat=1.21); \
done; \
git diff --exit-code;
.PHONY: mod-tidy
@@ -68,12 +67,12 @@ mod-tidy: ## Check go.mod tidiness
vet: ## Run "go vet"
set -e ; \
for dir in $(ALL_GO_MOD_DIRS); do \
cd "$${dir}"; \
echo ">>> Running 'go vet' for module: $${dir}"; \
go vet ./...; \
(cd "$${dir}" && go vet ./...); \
done;
.PHONY: vet
lint: ## Lint (using "golangci-lint")
golangci-lint run
.PHONY: lint

View File

@@ -10,7 +10,7 @@
# Official Sentry SDK for Go
[![Build Status](https://github.com/getsentry/sentry-go/workflows/go-workflow/badge.svg)](https://github.com/getsentry/sentry-go/actions?query=workflow%3Ago-workflow)
[![Build Status](https://github.com/getsentry/sentry-go/actions/workflows/test.yml/badge.svg)](https://github.com/getsentry/sentry-go/actions/workflows/test.yml)
[![Go Report Card](https://goreportcard.com/badge/github.com/getsentry/sentry-go)](https://goreportcard.com/report/github.com/getsentry/sentry-go)
[![Discord](https://img.shields.io/discord/621778831602221064)](https://discord.gg/Ww9hbqr)
[![go.dev](https://img.shields.io/badge/go.dev-pkg-007d9c.svg?style=flat)](https://pkg.go.dev/github.com/getsentry/sentry-go)
@@ -68,17 +68,20 @@ To get started, have a look at one of our [examples](_examples/):
We also provide a [complete API reference](https://pkg.go.dev/github.com/getsentry/sentry-go).
For more detailed information about how to get the most out of `sentry-go`,
checkout the official documentation:
check out the official documentation:
- [Sentry Go SDK documentation](https://docs.sentry.io/platforms/go/)
- Guides:
- [net/http](https://docs.sentry.io/platforms/go/guides/http/)
- [echo](https://docs.sentry.io/platforms/go/guides/echo/)
- [fasthttp](https://docs.sentry.io/platforms/go/guides/fasthttp/)
- [fiber](https://docs.sentry.io/platforms/go/guides/fiber/)
- [gin](https://docs.sentry.io/platforms/go/guides/gin/)
- [iris](https://docs.sentry.io/platforms/go/guides/iris/)
- [martini](https://docs.sentry.io/platforms/go/guides/martini/)
- [logrus](https://docs.sentry.io/platforms/go/guides/logrus/)
- [negroni](https://docs.sentry.io/platforms/go/guides/negroni/)
- [slog](https://docs.sentry.io/platforms/go/guides/slog/)
- [zerolog](https://docs.sentry.io/platforms/go/guides/zerolog/)
## Resources

View File

@@ -90,7 +90,7 @@ type EventProcessor func(event *Event, hint *EventHint) *Event
// ApplyToEvent changes an event based on external data and/or
// an event hint.
type EventModifier interface {
ApplyToEvent(event *Event, hint *EventHint) *Event
ApplyToEvent(event *Event, hint *EventHint, client *Client) *Event
}
var globalEventProcessors []EventProcessor
@@ -133,9 +133,6 @@ type ClientOptions struct {
TracesSampleRate float64
// Used to customize the sampling of traces, overrides TracesSampleRate.
TracesSampler TracesSampler
// The sample rate for profiling traces in the range [0.0, 1.0].
// This is relative to TracesSampleRate - it is a ratio of profiled traces out of all sampled traces.
ProfilesSampleRate float64
// List of regexp strings that will be used to match against event's message
// and if applicable, caught errors type and value.
// If the match is found, then a whole event will be dropped.
@@ -513,6 +510,14 @@ func (client *Client) Flush(timeout time.Duration) bool {
return client.Transport.Flush(timeout)
}
// Close clean up underlying Transport resources.
//
// Close should be called after Flush and before terminating the program
// otherwise some events may be lost.
func (client *Client) Close() {
client.Transport.Close()
}
// EventFromMessage creates an event from the given message string.
func (client *Client) EventFromMessage(message string, level Level) *Event {
if message == "" {
@@ -590,14 +595,6 @@ func (client *Client) GetSDKIdentifier() string {
return client.sdkIdentifier
}
// reverse reverses the slice a in place.
func reverse(a []Exception) {
for i := len(a)/2 - 1; i >= 0; i-- {
opp := len(a) - 1 - i
a[i], a[opp] = a[opp], a[i]
}
}
func (client *Client) processEvent(event *Event, hint *EventHint, scope EventModifier) *EventID {
if event == nil {
err := usageError{fmt.Errorf("%s called with nil event", callerFunctionName())}
@@ -685,7 +682,7 @@ func (client *Client) prepareEvent(event *Event, hint *EventHint, scope EventMod
}
if scope != nil {
event = scope.ApplyToEvent(event, hint)
event = scope.ApplyToEvent(event, hint, client)
if event == nil {
return nil
}
@@ -709,10 +706,6 @@ func (client *Client) prepareEvent(event *Event, hint *EventHint, scope EventMod
}
}
if event.sdkMetaData.transactionProfile != nil {
event.sdkMetaData.transactionProfile.UpdateFromEvent(event)
}
return event
}

View File

@@ -89,8 +89,8 @@ func NewDsn(rawURL string) (*Dsn, error) {
// Port
var port int
if parsedURL.Port() != "" {
port, err = strconv.Atoi(parsedURL.Port())
if p := parsedURL.Port(); p != "" {
port, err = strconv.Atoi(p)
if err != nil {
return nil, &DsnParseError{"invalid port"}
}

View File

@@ -39,8 +39,6 @@ func DynamicSamplingContextFromHeader(header []byte) (DynamicSamplingContext, er
}
func DynamicSamplingContextFromTransaction(span *Span) DynamicSamplingContext {
entries := map[string]string{}
hub := hubFromContext(span.Context())
scope := hub.Scope()
client := hub.Client()
@@ -52,6 +50,8 @@ func DynamicSamplingContextFromTransaction(span *Span) DynamicSamplingContext {
}
}
entries := make(map[string]string)
if traceID := span.TraceID.String(); traceID != "" {
entries["trace_id"] = traceID
}
@@ -78,20 +78,9 @@ func DynamicSamplingContextFromTransaction(span *Span) DynamicSamplingContext {
}
}
if userSegment := scope.user.Segment; userSegment != "" {
entries["user_segment"] = userSegment
}
entries["sampled"] = strconv.FormatBool(span.Sampled.Bool())
if span.Sampled.Bool() {
entries["sampled"] = "true"
} else {
entries["sampled"] = "false"
}
return DynamicSamplingContext{
Entries: entries,
Frozen: true,
}
return DynamicSamplingContext{Entries: entries, Frozen: true}
}
func (d DynamicSamplingContext) HasEntries() bool {
@@ -111,13 +100,55 @@ func (d DynamicSamplingContext) String() string {
}
members = append(members, member)
}
if len(members) > 0 {
baggage, err := baggage.New(members...)
if err != nil {
return ""
}
return baggage.String()
if len(members) == 0 {
return ""
}
return ""
baggage, err := baggage.New(members...)
if err != nil {
return ""
}
return baggage.String()
}
// Constructs a new DynamicSamplingContext using a scope and client. Accessing
// fields on the scope are not thread safe, and this function should only be
// called within scope methods.
func DynamicSamplingContextFromScope(scope *Scope, client *Client) DynamicSamplingContext {
entries := map[string]string{}
if client == nil || scope == nil {
return DynamicSamplingContext{
Entries: entries,
Frozen: false,
}
}
propagationContext := scope.propagationContext
if traceID := propagationContext.TraceID.String(); traceID != "" {
entries["trace_id"] = traceID
}
if sampleRate := client.options.TracesSampleRate; sampleRate != 0 {
entries["sample_rate"] = strconv.FormatFloat(sampleRate, 'f', -1, 64)
}
if dsn := client.dsn; dsn != nil {
if publicKey := dsn.publicKey; publicKey != "" {
entries["public_key"] = publicKey
}
}
if release := client.options.Release; release != "" {
entries["release"] = release
}
if environment := client.options.Environment; environment != "" {
entries["environment"] = environment
}
return DynamicSamplingContext{
Entries: entries,
Frozen: true,
}
}

View File

@@ -2,6 +2,7 @@ package sentry
import (
"context"
"fmt"
"sync"
"time"
)
@@ -293,8 +294,13 @@ func (hub *Hub) AddBreadcrumb(breadcrumb *Breadcrumb, hint *BreadcrumbHint) {
}
max := client.options.MaxBreadcrumbs
if max < 0 {
switch {
case max < 0:
return
case max == 0:
max = defaultMaxBreadcrumbs
case max > maxBreadcrumbs:
max = maxBreadcrumbs
}
if client.options.BeforeBreadcrumb != nil {
@@ -307,12 +313,6 @@ func (hub *Hub) AddBreadcrumb(breadcrumb *Breadcrumb, hint *BreadcrumbHint) {
}
}
if max == 0 {
max = defaultMaxBreadcrumbs
} else if max > maxBreadcrumbs {
max = maxBreadcrumbs
}
hub.Scope().AddBreadcrumb(breadcrumb, max)
}
@@ -365,6 +365,34 @@ func (hub *Hub) Flush(timeout time.Duration) bool {
return client.Flush(timeout)
}
// GetTraceparent returns the current Sentry traceparent string, to be used as a HTTP header value
// or HTML meta tag value.
// This function is context aware, as in it either returns the traceparent based
// on the current span, or the scope's propagation context.
func (hub *Hub) GetTraceparent() string {
scope := hub.Scope()
if scope.span != nil {
return scope.span.ToSentryTrace()
}
return fmt.Sprintf("%s-%s", scope.propagationContext.TraceID, scope.propagationContext.SpanID)
}
// GetBaggage returns the current Sentry baggage string, to be used as a HTTP header value
// or HTML meta tag value.
// This function is context aware, as in it either returns the baggage based
// on the current span or the scope's propagation context.
func (hub *Hub) GetBaggage() string {
scope := hub.Scope()
if scope.span != nil {
return scope.span.ToBaggage()
}
return scope.propagationContext.DynamicSamplingContext.String()
}
// HasHubOnContext checks whether Hub instance is bound to a given Context struct.
func HasHubOnContext(ctx context.Context) bool {
_, ok := ctx.Value(HubContextKey).(*Hub)

View File

@@ -8,6 +8,7 @@ import (
"net"
"net/http"
"reflect"
"slices"
"strings"
"time"
)
@@ -18,16 +19,9 @@ const eventType = "event"
// transactionType is the type of a transaction event.
const transactionType = "transaction"
// profileType is the type of a profile event.
// currently, profiles are always sent as part of a transaction event.
const profileType = "profile"
// checkInType is the type of a check in event.
const checkInType = "check_in"
// metricType is the type of a metric event.
const metricType = "statsd"
// Level marks the severity of the event.
type Level string
@@ -118,32 +112,27 @@ type User struct {
IPAddress string `json:"ip_address,omitempty"`
Username string `json:"username,omitempty"`
Name string `json:"name,omitempty"`
Segment string `json:"segment,omitempty"`
Data map[string]string `json:"data,omitempty"`
}
func (u User) IsEmpty() bool {
if len(u.ID) > 0 {
if u.ID != "" {
return false
}
if len(u.Email) > 0 {
if u.Email != "" {
return false
}
if len(u.IPAddress) > 0 {
if u.IPAddress != "" {
return false
}
if len(u.Username) > 0 {
if u.Username != "" {
return false
}
if len(u.Name) > 0 {
return false
}
if len(u.Segment) > 0 {
if u.Name != "" {
return false
}
@@ -166,10 +155,11 @@ type Request struct {
}
var sensitiveHeaders = map[string]struct{}{
"Authorization": {},
"Cookie": {},
"X-Forwarded-For": {},
"X-Real-Ip": {},
"Authorization": {},
"Proxy-Authorization": {},
"Cookie": {},
"X-Forwarded-For": {},
"X-Real-Ip": {},
}
// NewRequest returns a new Sentry Request from the given http.Request.
@@ -194,6 +184,7 @@ func NewRequest(r *http.Request) *Request {
// attach more than one Cookie header field.
cookies = r.Header.Get("Cookie")
headers = make(map[string]string, len(r.Header))
for k, v := range r.Header {
headers[k] = strings.Join(v, ",")
}
@@ -237,8 +228,7 @@ type Mechanism struct {
// SetUnhandled indicates that the exception is an unhandled exception, i.e.
// from a panic.
func (m *Mechanism) SetUnhandled() {
h := false
m.Handled = &h
m.Handled = Pointer(false)
}
// Exception specifies an error that occurred.
@@ -254,8 +244,7 @@ type Exception struct {
// SDKMetaData is a struct to stash data which is needed at some point in the SDK's event processing pipeline
// but which shouldn't get send to Sentry.
type SDKMetaData struct {
dsc DynamicSamplingContext
transactionProfile *profileInfo
dsc DynamicSamplingContext
}
// Contains information about how the name of the transaction was determined.
@@ -323,7 +312,6 @@ type Event struct {
Exception []Exception `json:"exception,omitempty"`
DebugMeta *DebugMeta `json:"debug_meta,omitempty"`
Attachments []*Attachment `json:"-"`
Metrics []Metric `json:"-"`
// The fields below are only relevant for transactions.
@@ -397,12 +385,13 @@ func (e *Event) SetException(exception error, maxErrorDepth int) {
}
// event.Exception should be sorted such that the most recent error is last.
reverse(e.Exception)
slices.Reverse(e.Exception)
for i := range e.Exception {
e.Exception[i].Mechanism = &Mechanism{
IsExceptionGroup: true,
ExceptionID: i,
Type: "generic",
}
if i == 0 {
continue
@@ -428,7 +417,9 @@ func (e *Event) MarshalJSON() ([]byte, error) {
// and a few type tricks.
if e.Type == transactionType {
return e.transactionMarshalJSON()
} else if e.Type == checkInType {
}
if e.Type == checkInType {
return e.checkInMarshalJSON()
}
return e.defaultMarshalJSON()

View File

@@ -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
```

View File

@@ -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
}

View File

@@ -1,427 +0,0 @@
package sentry
import (
"fmt"
"hash/crc32"
"math"
"regexp"
"sort"
"strings"
)
type (
NumberOrString interface {
int | string
}
void struct{}
)
var (
member void
keyRegex = regexp.MustCompile(`[^a-zA-Z0-9_/.-]+`)
valueRegex = regexp.MustCompile(`[^\w\d\s_:/@\.{}\[\]$-]+`)
unitRegex = regexp.MustCompile(`[^a-z]+`)
)
type MetricUnit struct {
unit string
}
func (m MetricUnit) toString() string {
return m.unit
}
func NanoSecond() MetricUnit {
return MetricUnit{
"nanosecond",
}
}
func MicroSecond() MetricUnit {
return MetricUnit{
"microsecond",
}
}
func MilliSecond() MetricUnit {
return MetricUnit{
"millisecond",
}
}
func Second() MetricUnit {
return MetricUnit{
"second",
}
}
func Minute() MetricUnit {
return MetricUnit{
"minute",
}
}
func Hour() MetricUnit {
return MetricUnit{
"hour",
}
}
func Day() MetricUnit {
return MetricUnit{
"day",
}
}
func Week() MetricUnit {
return MetricUnit{
"week",
}
}
func Bit() MetricUnit {
return MetricUnit{
"bit",
}
}
func Byte() MetricUnit {
return MetricUnit{
"byte",
}
}
func KiloByte() MetricUnit {
return MetricUnit{
"kilobyte",
}
}
func KibiByte() MetricUnit {
return MetricUnit{
"kibibyte",
}
}
func MegaByte() MetricUnit {
return MetricUnit{
"megabyte",
}
}
func MebiByte() MetricUnit {
return MetricUnit{
"mebibyte",
}
}
func GigaByte() MetricUnit {
return MetricUnit{
"gigabyte",
}
}
func GibiByte() MetricUnit {
return MetricUnit{
"gibibyte",
}
}
func TeraByte() MetricUnit {
return MetricUnit{
"terabyte",
}
}
func TebiByte() MetricUnit {
return MetricUnit{
"tebibyte",
}
}
func PetaByte() MetricUnit {
return MetricUnit{
"petabyte",
}
}
func PebiByte() MetricUnit {
return MetricUnit{
"pebibyte",
}
}
func ExaByte() MetricUnit {
return MetricUnit{
"exabyte",
}
}
func ExbiByte() MetricUnit {
return MetricUnit{
"exbibyte",
}
}
func Ratio() MetricUnit {
return MetricUnit{
"ratio",
}
}
func Percent() MetricUnit {
return MetricUnit{
"percent",
}
}
func CustomUnit(unit string) MetricUnit {
return MetricUnit{
unitRegex.ReplaceAllString(unit, ""),
}
}
type Metric interface {
GetType() string
GetTags() map[string]string
GetKey() string
GetUnit() string
GetTimestamp() int64
SerializeValue() string
SerializeTags() string
}
type abstractMetric struct {
key string
unit MetricUnit
tags map[string]string
// A unix timestamp (full seconds elapsed since 1970-01-01 00:00 UTC).
timestamp int64
}
func (am abstractMetric) GetTags() map[string]string {
return am.tags
}
func (am abstractMetric) GetKey() string {
return am.key
}
func (am abstractMetric) GetUnit() string {
return am.unit.toString()
}
func (am abstractMetric) GetTimestamp() int64 {
return am.timestamp
}
func (am abstractMetric) SerializeTags() string {
var sb strings.Builder
values := make([]string, 0, len(am.tags))
for k := range am.tags {
values = append(values, k)
}
sortSlice(values)
for _, key := range values {
val := sanitizeValue(am.tags[key])
key = sanitizeKey(key)
sb.WriteString(fmt.Sprintf("%s:%s,", key, val))
}
s := sb.String()
if len(s) > 0 {
s = s[:len(s)-1]
}
return s
}
// Counter Metric.
type CounterMetric struct {
value float64
abstractMetric
}
func (c *CounterMetric) Add(value float64) {
c.value += value
}
func (c CounterMetric) GetType() string {
return "c"
}
func (c CounterMetric) SerializeValue() string {
return fmt.Sprintf(":%v", c.value)
}
// timestamp: A unix timestamp (full seconds elapsed since 1970-01-01 00:00 UTC).
func NewCounterMetric(key string, unit MetricUnit, tags map[string]string, timestamp int64, value float64) CounterMetric {
am := abstractMetric{
key,
unit,
tags,
timestamp,
}
return CounterMetric{
value,
am,
}
}
// Distribution Metric.
type DistributionMetric struct {
values []float64
abstractMetric
}
func (d *DistributionMetric) Add(value float64) {
d.values = append(d.values, value)
}
func (d DistributionMetric) GetType() string {
return "d"
}
func (d DistributionMetric) SerializeValue() string {
var sb strings.Builder
for _, el := range d.values {
sb.WriteString(fmt.Sprintf(":%v", el))
}
return sb.String()
}
// timestamp: A unix timestamp (full seconds elapsed since 1970-01-01 00:00 UTC).
func NewDistributionMetric(key string, unit MetricUnit, tags map[string]string, timestamp int64, value float64) DistributionMetric {
am := abstractMetric{
key,
unit,
tags,
timestamp,
}
return DistributionMetric{
[]float64{value},
am,
}
}
// Gauge Metric.
type GaugeMetric struct {
last float64
min float64
max float64
sum float64
count float64
abstractMetric
}
func (g *GaugeMetric) Add(value float64) {
g.last = value
g.min = math.Min(g.min, value)
g.max = math.Max(g.max, value)
g.sum += value
g.count++
}
func (g GaugeMetric) GetType() string {
return "g"
}
func (g GaugeMetric) SerializeValue() string {
return fmt.Sprintf(":%v:%v:%v:%v:%v", g.last, g.min, g.max, g.sum, g.count)
}
// timestamp: A unix timestamp (full seconds elapsed since 1970-01-01 00:00 UTC).
func NewGaugeMetric(key string, unit MetricUnit, tags map[string]string, timestamp int64, value float64) GaugeMetric {
am := abstractMetric{
key,
unit,
tags,
timestamp,
}
return GaugeMetric{
value, // last
value, // min
value, // max
value, // sum
value, // count
am,
}
}
// Set Metric.
type SetMetric[T NumberOrString] struct {
values map[T]void
abstractMetric
}
func (s *SetMetric[T]) Add(value T) {
s.values[value] = member
}
func (s SetMetric[T]) GetType() string {
return "s"
}
func (s SetMetric[T]) SerializeValue() string {
_hash := func(s string) uint32 {
return crc32.ChecksumIEEE([]byte(s))
}
values := make([]T, 0, len(s.values))
for k := range s.values {
values = append(values, k)
}
sortSlice(values)
var sb strings.Builder
for _, el := range values {
switch any(el).(type) {
case int:
sb.WriteString(fmt.Sprintf(":%v", el))
case string:
s := fmt.Sprintf("%v", el)
sb.WriteString(fmt.Sprintf(":%d", _hash(s)))
}
}
return sb.String()
}
// timestamp: A unix timestamp (full seconds elapsed since 1970-01-01 00:00 UTC).
func NewSetMetric[T NumberOrString](key string, unit MetricUnit, tags map[string]string, timestamp int64, value T) SetMetric[T] {
am := abstractMetric{
key,
unit,
tags,
timestamp,
}
return SetMetric[T]{
map[T]void{
value: member,
},
am,
}
}
func sanitizeKey(s string) string {
return keyRegex.ReplaceAllString(s, "_")
}
func sanitizeValue(s string) string {
return valueRegex.ReplaceAllString(s, "")
}
type Ordered interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | ~float32 | ~float64 | ~string
}
func sortSlice[T Ordered](s []T) {
sort.Slice(s, func(i, j int) bool {
return s[i] < s[j]
})
}

View File

@@ -1,73 +0,0 @@
package sentry
// Based on https://github.com/getsentry/vroom/blob/d11c26063e802d66b9a592c4010261746ca3dfa4/internal/sample/sample.go
import (
"time"
)
type (
profileDevice struct {
Architecture string `json:"architecture"`
Classification string `json:"classification"`
Locale string `json:"locale"`
Manufacturer string `json:"manufacturer"`
Model string `json:"model"`
}
profileOS struct {
BuildNumber string `json:"build_number"`
Name string `json:"name"`
Version string `json:"version"`
}
profileRuntime struct {
Name string `json:"name"`
Version string `json:"version"`
}
profileSample struct {
ElapsedSinceStartNS uint64 `json:"elapsed_since_start_ns"`
StackID int `json:"stack_id"`
ThreadID uint64 `json:"thread_id"`
}
profileThreadMetadata struct {
Name string `json:"name,omitempty"`
Priority int `json:"priority,omitempty"`
}
profileStack []int
profileTrace struct {
Frames []*Frame `json:"frames"`
Samples []profileSample `json:"samples"`
Stacks []profileStack `json:"stacks"`
ThreadMetadata map[uint64]*profileThreadMetadata `json:"thread_metadata"`
}
profileInfo struct {
DebugMeta *DebugMeta `json:"debug_meta,omitempty"`
Device profileDevice `json:"device"`
Environment string `json:"environment,omitempty"`
EventID string `json:"event_id"`
OS profileOS `json:"os"`
Platform string `json:"platform"`
Release string `json:"release"`
Dist string `json:"dist"`
Runtime profileRuntime `json:"runtime"`
Timestamp time.Time `json:"timestamp"`
Trace *profileTrace `json:"profile"`
Transaction profileTransaction `json:"transaction"`
Version string `json:"version"`
}
// see https://github.com/getsentry/vroom/blob/a91e39416723ec44fc54010257020eeaf9a77cbd/internal/transaction/transaction.go
profileTransaction struct {
ActiveThreadID uint64 `json:"active_thread_id"`
DurationNS uint64 `json:"duration_ns,omitempty"`
ID EventID `json:"id"`
Name string `json:"name"`
TraceID string `json:"trace_id"`
}
)

View File

@@ -1,451 +0,0 @@
package sentry
import (
"container/ring"
"strconv"
"runtime"
"sync"
"sync/atomic"
"time"
"github.com/getsentry/sentry-go/internal/traceparser"
)
// Start a profiler that collects samples continuously, with a buffer of up to 30 seconds.
// Later, you can collect a slice from this buffer, producing a Trace.
func startProfiling(startTime time.Time) profiler {
onProfilerStart()
p := newProfiler(startTime)
// Wait for the profiler to finish setting up before returning to the caller.
started := make(chan struct{})
go p.run(started)
if _, ok := <-started; ok {
return p
}
return nil
}
type profiler interface {
// GetSlice returns a slice of the profiled data between the given times.
GetSlice(startTime, endTime time.Time) *profilerResult
Stop(wait bool)
}
type profilerResult struct {
callerGoID uint64
trace *profileTrace
}
func getCurrentGoID() uint64 {
// We shouldn't panic but let's be super safe.
defer func() {
if err := recover(); err != nil {
Logger.Printf("Profiler panic in getCurrentGoID(): %v\n", err)
}
}()
// Buffer to read the stack trace into. We should be good with a small buffer because we only need the first line.
var stacksBuffer = make([]byte, 100)
var n = runtime.Stack(stacksBuffer, false)
if n > 0 {
var traces = traceparser.Parse(stacksBuffer[0:n])
if traces.Length() > 0 {
var trace = traces.Item(0)
return trace.GoID()
}
}
return 0
}
const profilerSamplingRateHz = 101 // 101 Hz; not 100 Hz because of the lockstep sampling (https://stackoverflow.com/a/45471031/1181370)
const profilerSamplingRate = time.Second / profilerSamplingRateHz
const stackBufferMaxGrowth = 512 * 1024
const stackBufferLimit = 10 * 1024 * 1024
const profilerRuntimeLimit = 30 // seconds
type profileRecorder struct {
startTime time.Time
stopSignal chan struct{}
stopped int64
mutex sync.RWMutex
testProfilerPanic int64
// Map from runtime.StackRecord.Stack0 to an index in stacks.
stackIndexes map[string]int
stacks []profileStack
newStacks []profileStack // New stacks created in the current interation.
stackKeyBuffer []byte
// Map from runtime.Frame.PC to an index in frames.
frameIndexes map[string]int
frames []*Frame
newFrames []*Frame // New frames created in the current interation.
// We keep a ring buffer of 30 seconds worth of samples, so that we can later slice it.
// Each bucket is a slice of samples all taken at the same time.
samplesBucketsHead *ring.Ring
// Buffer to read current stacks - will grow automatically up to stackBufferLimit.
stacksBuffer []byte
}
func newProfiler(startTime time.Time) *profileRecorder {
// Pre-allocate the profile trace for the currently active number of routines & 100 ms worth of samples.
// Other coefficients are just guesses of what might be a good starting point to avoid allocs on short runs.
return &profileRecorder{
startTime: startTime,
stopSignal: make(chan struct{}, 1),
stackIndexes: make(map[string]int, 32),
stacks: make([]profileStack, 0, 32),
newStacks: make([]profileStack, 0, 32),
frameIndexes: make(map[string]int, 128),
frames: make([]*Frame, 0, 128),
newFrames: make([]*Frame, 0, 128),
samplesBucketsHead: ring.New(profilerRuntimeLimit * profilerSamplingRateHz),
// A buffer of 2 KiB per goroutine stack looks like a good starting point (empirically determined).
stacksBuffer: make([]byte, runtime.NumGoroutine()*2048),
}
}
// This allows us to test whether panic during profiling are handled correctly and don't block execution.
// If the number is lower than 0, profilerGoroutine() will panic immedately.
// If the number is higher than 0, profiler.onTick() will panic when the given samples-set index is being collected.
var testProfilerPanic int64
var profilerRunning int64
func (p *profileRecorder) run(started chan struct{}) {
// Code backup for manual test debugging:
// if !atomic.CompareAndSwapInt64(&profilerRunning, 0, 1) {
// panic("Only one profiler can be running at a time")
// }
// We shouldn't panic but let's be super safe.
defer func() {
if err := recover(); err != nil {
Logger.Printf("Profiler panic in run(): %v\n", err)
}
atomic.StoreInt64(&testProfilerPanic, 0)
close(started)
p.stopSignal <- struct{}{}
atomic.StoreInt64(&p.stopped, 1)
atomic.StoreInt64(&profilerRunning, 0)
}()
p.testProfilerPanic = atomic.LoadInt64(&testProfilerPanic)
if p.testProfilerPanic < 0 {
Logger.Printf("Profiler panicking during startup because testProfilerPanic == %v\n", p.testProfilerPanic)
panic("This is an expected panic in profilerGoroutine() during tests")
}
// Collect the first sample immediately.
p.onTick()
// Periodically collect stacks, starting after profilerSamplingRate has passed.
collectTicker := profilerTickerFactory(profilerSamplingRate)
defer collectTicker.Stop()
var tickerChannel = collectTicker.TickSource()
started <- struct{}{}
for {
select {
case <-tickerChannel:
p.onTick()
collectTicker.Ticked()
case <-p.stopSignal:
return
}
}
}
func (p *profileRecorder) Stop(wait bool) {
if atomic.LoadInt64(&p.stopped) == 1 {
return
}
p.stopSignal <- struct{}{}
if wait {
<-p.stopSignal
}
}
func (p *profileRecorder) GetSlice(startTime, endTime time.Time) *profilerResult {
// Unlikely edge cases - profiler wasn't running at all or the given times are invalid in relation to each other.
if p.startTime.After(endTime) || startTime.After(endTime) {
return nil
}
var relativeStartNS = uint64(0)
if p.startTime.Before(startTime) {
relativeStartNS = uint64(startTime.Sub(p.startTime).Nanoseconds())
}
var relativeEndNS = uint64(endTime.Sub(p.startTime).Nanoseconds())
samplesCount, bucketsReversed, trace := p.getBuckets(relativeStartNS, relativeEndNS)
if samplesCount == 0 {
return nil
}
var result = &profilerResult{
callerGoID: getCurrentGoID(),
trace: trace,
}
trace.Samples = make([]profileSample, samplesCount)
trace.ThreadMetadata = make(map[uint64]*profileThreadMetadata, len(bucketsReversed[0].goIDs))
var s = samplesCount - 1
for _, bucket := range bucketsReversed {
var elapsedSinceStartNS = bucket.relativeTimeNS - relativeStartNS
for i, goID := range bucket.goIDs {
trace.Samples[s].ElapsedSinceStartNS = elapsedSinceStartNS
trace.Samples[s].ThreadID = goID
trace.Samples[s].StackID = bucket.stackIDs[i]
s--
if _, goroutineExists := trace.ThreadMetadata[goID]; !goroutineExists {
trace.ThreadMetadata[goID] = &profileThreadMetadata{
Name: "Goroutine " + strconv.FormatUint(goID, 10),
}
}
}
}
return result
}
// Collect all buckets of samples in the given time range while holding a read lock.
func (p *profileRecorder) getBuckets(relativeStartNS, relativeEndNS uint64) (samplesCount int, buckets []*profileSamplesBucket, trace *profileTrace) {
p.mutex.RLock()
defer p.mutex.RUnlock()
// sampleBucketsHead points at the last stored bucket so it's a good starting point to search backwards for the end.
var end = p.samplesBucketsHead
for end.Value != nil && end.Value.(*profileSamplesBucket).relativeTimeNS > relativeEndNS {
end = end.Prev()
}
// Edge case - no items stored before the given endTime.
if end.Value == nil {
return 0, nil, nil
}
{ // Find the first item after the given startTime.
var start = end
var prevBucket *profileSamplesBucket
samplesCount = 0
buckets = make([]*profileSamplesBucket, 0, int64((relativeEndNS-relativeStartNS)/uint64(profilerSamplingRate.Nanoseconds()))+1)
for start.Value != nil {
var bucket = start.Value.(*profileSamplesBucket)
// If this bucket's time is before the requests start time, don't collect it (and stop iterating further).
if bucket.relativeTimeNS < relativeStartNS {
break
}
// If this bucket time is greater than previous the bucket's time, we have exhausted the whole ring buffer
// before we were able to find the start time. That means the start time is not present and we must break.
// This happens if the slice duration exceeds the ring buffer capacity.
if prevBucket != nil && bucket.relativeTimeNS > prevBucket.relativeTimeNS {
break
}
samplesCount += len(bucket.goIDs)
buckets = append(buckets, bucket)
start = start.Prev()
prevBucket = bucket
}
}
// Edge case - if the period requested was too short and we haven't collected enough samples.
if len(buckets) < 2 {
return 0, nil, nil
}
trace = &profileTrace{
Frames: p.frames,
Stacks: p.stacks,
}
return samplesCount, buckets, trace
}
func (p *profileRecorder) onTick() {
elapsedNs := time.Since(p.startTime).Nanoseconds()
if p.testProfilerPanic > 0 {
Logger.Printf("Profiler testProfilerPanic == %v\n", p.testProfilerPanic)
if p.testProfilerPanic == 1 {
Logger.Println("Profiler panicking onTick()")
panic("This is an expected panic in Profiler.OnTick() during tests")
}
p.testProfilerPanic--
}
records := p.collectRecords()
p.processRecords(uint64(elapsedNs), records)
// Free up some memory if we don't need such a large buffer anymore.
if len(p.stacksBuffer) > len(records)*3 {
p.stacksBuffer = make([]byte, len(records)*3)
}
}
func (p *profileRecorder) collectRecords() []byte {
for {
// Capture stacks for all existing goroutines.
// Note: runtime.GoroutineProfile() would be better but we can't use it at the moment because
// it doesn't give us `gid` for each routine, see https://github.com/golang/go/issues/59663
n := runtime.Stack(p.stacksBuffer, true)
// If we couldn't read everything, increase the buffer and try again.
if n >= len(p.stacksBuffer) && n < stackBufferLimit {
var newSize = n * 2
if newSize > n+stackBufferMaxGrowth {
newSize = n + stackBufferMaxGrowth
}
if newSize > stackBufferLimit {
newSize = stackBufferLimit
}
p.stacksBuffer = make([]byte, newSize)
} else {
return p.stacksBuffer[0:n]
}
}
}
func (p *profileRecorder) processRecords(elapsedNs uint64, stacksBuffer []byte) {
var traces = traceparser.Parse(stacksBuffer)
var length = traces.Length()
// Shouldn't happen but let's be safe and don't store empty buckets.
if length == 0 {
return
}
var bucket = &profileSamplesBucket{
relativeTimeNS: elapsedNs,
stackIDs: make([]int, length),
goIDs: make([]uint64, length),
}
// reset buffers
p.newFrames = p.newFrames[:0]
p.newStacks = p.newStacks[:0]
for i := 0; i < length; i++ {
var stack = traces.Item(i)
bucket.stackIDs[i] = p.addStackTrace(stack)
bucket.goIDs[i] = stack.GoID()
}
p.mutex.Lock()
defer p.mutex.Unlock()
p.stacks = append(p.stacks, p.newStacks...)
p.frames = append(p.frames, p.newFrames...)
p.samplesBucketsHead = p.samplesBucketsHead.Next()
p.samplesBucketsHead.Value = bucket
}
func (p *profileRecorder) addStackTrace(capturedStack traceparser.Trace) int {
iter := capturedStack.Frames()
stack := make(profileStack, 0, iter.LengthUpperBound())
// Originally, we've used `capturedStack.UniqueIdentifier()` as a key but that was incorrect because it also
// contains function arguments and we want to group stacks by function name and file/line only.
// Instead, we need to parse frames and we use a list of their indexes as a key.
// We reuse the same buffer for each stack to avoid allocations; this is a hot spot.
var expectedBufferLen = cap(stack) * 5 // 4 bytes per frame + 1 byte for space
if cap(p.stackKeyBuffer) < expectedBufferLen {
p.stackKeyBuffer = make([]byte, 0, expectedBufferLen)
} else {
p.stackKeyBuffer = p.stackKeyBuffer[:0]
}
for iter.HasNext() {
var frame = iter.Next()
if frameIndex := p.addFrame(frame); frameIndex >= 0 {
stack = append(stack, frameIndex)
p.stackKeyBuffer = append(p.stackKeyBuffer, 0) // space
// The following code is just like binary.AppendUvarint() which isn't yet available in Go 1.18.
x := uint64(frameIndex) + 1
for x >= 0x80 {
p.stackKeyBuffer = append(p.stackKeyBuffer, byte(x)|0x80)
x >>= 7
}
p.stackKeyBuffer = append(p.stackKeyBuffer, byte(x))
}
}
stackIndex, exists := p.stackIndexes[string(p.stackKeyBuffer)]
if !exists {
stackIndex = len(p.stacks) + len(p.newStacks)
p.newStacks = append(p.newStacks, stack)
p.stackIndexes[string(p.stackKeyBuffer)] = stackIndex
}
return stackIndex
}
func (p *profileRecorder) addFrame(capturedFrame traceparser.Frame) int {
// NOTE: Don't convert to string yet, it's expensive and compiler can avoid it when
// indexing into a map (only needs a copy when adding a new key to the map).
var key = capturedFrame.UniqueIdentifier()
frameIndex, exists := p.frameIndexes[string(key)]
if !exists {
module, function := splitQualifiedFunctionName(string(capturedFrame.Func()))
file, line := capturedFrame.File()
frame := newFrame(module, function, string(file), line)
frameIndex = len(p.frames) + len(p.newFrames)
p.newFrames = append(p.newFrames, &frame)
p.frameIndexes[string(key)] = frameIndex
}
return frameIndex
}
type profileSamplesBucket struct {
relativeTimeNS uint64
stackIDs []int
goIDs []uint64
}
// A Ticker holds a channel that delivers “ticks” of a clock at intervals.
type profilerTicker interface {
// Stop turns off a ticker. After Stop, no more ticks will be sent.
Stop()
// TickSource returns a read-only channel of ticks.
TickSource() <-chan time.Time
// Ticked is called by the Profiler after a tick is processed to notify the ticker. Used for testing.
Ticked()
}
type timeTicker struct {
*time.Ticker
}
func (t *timeTicker) TickSource() <-chan time.Time {
return t.C
}
func (t *timeTicker) Ticked() {}
func profilerTickerFactoryDefault(d time.Duration) profilerTicker {
return &timeTicker{time.NewTicker(d)}
}
// We allow overriding the ticker for tests. CI is terribly flaky
// because the time.Ticker doesn't guarantee regular ticks - they may come (a lot) later than the given interval.
var profilerTickerFactory = profilerTickerFactoryDefault

View File

@@ -1,5 +0,0 @@
//go:build !windows
package sentry
func onProfilerStart() {}

View File

@@ -1,24 +0,0 @@
package sentry
import (
"sync"
"syscall"
)
// This works around the ticker resolution on Windows being ~15ms by default.
// See https://github.com/golang/go/issues/44343
func setTimeTickerResolution() {
var winmmDLL = syscall.NewLazyDLL("winmm.dll")
if winmmDLL != nil {
var timeBeginPeriod = winmmDLL.NewProc("timeBeginPeriod")
if timeBeginPeriod != nil {
timeBeginPeriod.Call(uintptr(1))
}
}
}
var setupTickerResolutionOnce sync.Once
func onProfilerStart() {
setupTickerResolutionOnce.Do(setTimeTickerResolution)
}

View File

@@ -0,0 +1,90 @@
package sentry
import (
"crypto/rand"
"encoding/json"
)
type PropagationContext struct {
TraceID TraceID `json:"trace_id"`
SpanID SpanID `json:"span_id"`
ParentSpanID SpanID `json:"parent_span_id"`
DynamicSamplingContext DynamicSamplingContext `json:"-"`
}
func (p PropagationContext) MarshalJSON() ([]byte, error) {
type propagationContext PropagationContext
var parentSpanID string
if p.ParentSpanID != zeroSpanID {
parentSpanID = p.ParentSpanID.String()
}
return json.Marshal(struct {
*propagationContext
ParentSpanID string `json:"parent_span_id,omitempty"`
}{
propagationContext: (*propagationContext)(&p),
ParentSpanID: parentSpanID,
})
}
func (p PropagationContext) Map() map[string]interface{} {
m := map[string]interface{}{
"trace_id": p.TraceID,
"span_id": p.SpanID,
}
if p.ParentSpanID != zeroSpanID {
m["parent_span_id"] = p.ParentSpanID
}
return m
}
func NewPropagationContext() PropagationContext {
p := PropagationContext{}
if _, err := rand.Read(p.TraceID[:]); err != nil {
panic(err)
}
if _, err := rand.Read(p.SpanID[:]); err != nil {
panic(err)
}
return p
}
func PropagationContextFromHeaders(trace, baggage string) (PropagationContext, error) {
p := NewPropagationContext()
if _, err := rand.Read(p.SpanID[:]); err != nil {
panic(err)
}
hasTrace := false
if trace != "" {
if tpc, valid := ParseTraceParentContext([]byte(trace)); valid {
hasTrace = true
p.TraceID = tpc.TraceID
p.ParentSpanID = tpc.ParentSpanID
}
}
if baggage != "" {
dsc, err := DynamicSamplingContextFromHeader([]byte(baggage))
if err != nil {
return PropagationContext{}, err
}
p.DynamicSamplingContext = dsc
}
// In case a sentry-trace header is present but there are no sentry-related
// values in the baggage, create an empty, frozen DynamicSamplingContext.
if hasTrace && !p.DynamicSamplingContext.HasEntries() {
p.DynamicSamplingContext = DynamicSamplingContext{
Frozen: true,
}
}
return p, nil
}

View File

@@ -43,20 +43,22 @@ type Scope struct {
Overflow() bool
}
eventProcessors []EventProcessor
propagationContext PropagationContext
span *Span
}
// NewScope creates a new Scope.
func NewScope() *Scope {
scope := Scope{
breadcrumbs: make([]*Breadcrumb, 0),
attachments: make([]*Attachment, 0),
tags: make(map[string]string),
contexts: make(map[string]Context),
extra: make(map[string]interface{}),
fingerprint: make([]string, 0),
return &Scope{
breadcrumbs: make([]*Breadcrumb, 0),
attachments: make([]*Attachment, 0),
tags: make(map[string]string),
contexts: make(map[string]Context),
extra: make(map[string]interface{}),
fingerprint: make([]string, 0),
propagationContext: NewPropagationContext(),
}
return &scope
}
// AddBreadcrumb adds new breadcrumb to the current scope
@@ -292,6 +294,22 @@ func (scope *Scope) SetLevel(level Level) {
scope.level = level
}
// SetPropagationContext sets the propagation context for the current scope.
func (scope *Scope) SetPropagationContext(propagationContext PropagationContext) {
scope.mu.Lock()
defer scope.mu.Unlock()
scope.propagationContext = propagationContext
}
// SetSpan sets a span for the current scope.
func (scope *Scope) SetSpan(span *Span) {
scope.mu.Lock()
defer scope.mu.Unlock()
scope.span = span
}
// Clone returns a copy of the current scope with all data copied over.
func (scope *Scope) Clone() *Scope {
scope.mu.RLock()
@@ -318,6 +336,8 @@ func (scope *Scope) Clone() *Scope {
clone.request = scope.request
clone.requestBody = scope.requestBody
clone.eventProcessors = scope.eventProcessors
clone.propagationContext = scope.propagationContext
clone.span = scope.span
return clone
}
@@ -335,7 +355,7 @@ func (scope *Scope) AddEventProcessor(processor EventProcessor) {
}
// ApplyToEvent takes the data from the current scope and attaches it to the event.
func (scope *Scope) ApplyToEvent(event *Event, hint *EventHint) *Event {
func (scope *Scope) ApplyToEvent(event *Event, hint *EventHint, client *Client) *Event {
scope.mu.RLock()
defer scope.mu.RUnlock()
@@ -379,6 +399,29 @@ func (scope *Scope) ApplyToEvent(event *Event, hint *EventHint) *Event {
}
}
if event.Contexts == nil {
event.Contexts = make(map[string]Context)
}
if scope.span != nil {
if _, ok := event.Contexts["trace"]; !ok {
event.Contexts["trace"] = scope.span.traceContext().Map()
}
transaction := scope.span.GetTransaction()
if transaction != nil {
event.sdkMetaData.dsc = DynamicSamplingContextFromTransaction(transaction)
}
} else {
event.Contexts["trace"] = scope.propagationContext.Map()
dsc := scope.propagationContext.DynamicSamplingContext
if !dsc.HasEntries() && client != nil {
dsc = DynamicSamplingContextFromScope(scope, client)
}
event.sdkMetaData.dsc = dsc
}
if len(scope.extra) > 0 {
if event.Extra == nil {
event.Extra = make(map[string]interface{}, len(scope.extra))
@@ -435,7 +478,7 @@ func (scope *Scope) ApplyToEvent(event *Event, hint *EventHint) *Event {
// a proper deep copy: if some context values are pointer types (e.g. maps),
// they won't be properly copied.
func cloneContext(c Context) Context {
res := Context{}
res := make(Context, len(c))
for k, v := range c {
res[k] = v
}

View File

@@ -6,7 +6,7 @@ import (
)
// The version of the SDK.
const SDKVersion = "0.28.1"
const SDKVersion = "0.31.1"
// apiVersion is the minimum version of the Sentry API compatible with the
// sentry-go SDK.

View File

@@ -4,6 +4,7 @@ import (
"go/build"
"reflect"
"runtime"
"slices"
"strings"
)
@@ -277,12 +278,7 @@ func extractFrames(pcs []uintptr) []runtime.Frame {
}
}
// TODO don't append and reverse, put in the right place from the start.
// reverse
for i, j := 0, len(frames)-1; i < j; i, j = i+1, j-1 {
frames[i], frames[j] = frames[j], frames[i]
}
slices.Reverse(frames)
return frames
}
@@ -307,6 +303,9 @@ func createFrames(frames []runtime.Frame) []Frame {
}
}
// Fix issues grouping errors with the new fully qualified function names
// introduced from Go 1.21
result = cleanupFunctionNamePrefix(result)
return result
}
@@ -333,12 +332,10 @@ func shouldSkipFrame(module string) bool {
var goRoot = strings.ReplaceAll(build.Default.GOROOT, "\\", "/")
func setInAppFrame(frame *Frame) {
if strings.HasPrefix(frame.AbsPath, goRoot) ||
strings.Contains(frame.Module, "vendor") ||
frame.InApp = true
if strings.HasPrefix(frame.AbsPath, goRoot) || strings.Contains(frame.Module, "vendor") ||
strings.Contains(frame.Module, "third_party") {
frame.InApp = false
} else {
frame.InApp = true
}
}
@@ -379,3 +376,32 @@ func baseName(name string) string {
}
return name
}
func isCompilerGeneratedSymbol(name string) bool {
// In versions of Go 1.20 and above a prefix of "type:" and "go:" is a
// compiler-generated symbol that doesn't belong to any package.
// See variable reservedimports in cmd/compile/internal/gc/subr.go
if strings.HasPrefix(name, "go:") || strings.HasPrefix(name, "type:") {
return true
}
return false
}
// Walk backwards through the results and for the current function name
// remove it's parent function's prefix, leaving only it's actual name. This
// fixes issues grouping errors with the new fully qualified function names
// introduced from Go 1.21.
func cleanupFunctionNamePrefix(f []Frame) []Frame {
for i := len(f) - 1; i > 0; i-- {
name := f[i].Function
parentName := f[i-1].Function + "."
if !strings.HasPrefix(name, parentName) {
continue
}
f[i].Function = name[len(parentName):]
}
return f
}

View File

@@ -1,15 +0,0 @@
//go:build !go1.20
package sentry
import "strings"
func isCompilerGeneratedSymbol(name string) bool {
// In versions of Go below 1.20 a prefix of "type." and "go." is a
// compiler-generated symbol that doesn't belong to any package.
// See variable reservedimports in cmd/compile/internal/gc/subr.go
if strings.HasPrefix(name, "go.") || strings.HasPrefix(name, "type.") {
return true
}
return false
}

View File

@@ -1,15 +0,0 @@
//go:build go1.20
package sentry
import "strings"
func isCompilerGeneratedSymbol(name string) bool {
// In versions of Go 1.20 and above a prefix of "type:" and "go:" is a
// compiler-generated symbol that doesn't belong to any package.
// See variable reservedimports in cmd/compile/internal/gc/subr.go
if strings.HasPrefix(name, "go:") || strings.HasPrefix(name, "type:") {
return true
}
return false
}

View File

@@ -1,95 +0,0 @@
package sentry
import (
"sync"
"time"
)
// Checks whether the transaction should be profiled (according to ProfilesSampleRate)
// and starts a profiler if so.
func (s *Span) sampleTransactionProfile() {
var sampleRate = s.clientOptions().ProfilesSampleRate
switch {
case sampleRate < 0.0 || sampleRate > 1.0:
Logger.Printf("Skipping transaction profiling: ProfilesSampleRate out of range [0.0, 1.0]: %f\n", sampleRate)
case sampleRate == 0.0 || rng.Float64() >= sampleRate:
Logger.Printf("Skipping transaction profiling: ProfilesSampleRate is: %f\n", sampleRate)
default:
startProfilerOnce.Do(startGlobalProfiler)
if globalProfiler == nil {
Logger.Println("Skipping transaction profiling: the profiler couldn't be started")
} else {
s.collectProfile = collectTransactionProfile
}
}
}
// transactionProfiler collects a profile for a given span.
type transactionProfiler func(span *Span) *profileInfo
var startProfilerOnce sync.Once
var globalProfiler profiler
func startGlobalProfiler() {
globalProfiler = startProfiling(time.Now())
}
func collectTransactionProfile(span *Span) *profileInfo {
result := globalProfiler.GetSlice(span.StartTime, span.EndTime)
if result == nil || result.trace == nil {
return nil
}
info := &profileInfo{
Version: "1",
EventID: uuid(),
// See https://github.com/getsentry/sentry-go/pull/626#discussion_r1204870340 for explanation why we use the Transaction time.
Timestamp: span.StartTime,
Trace: result.trace,
Transaction: profileTransaction{
DurationNS: uint64(span.EndTime.Sub(span.StartTime).Nanoseconds()),
Name: span.Name,
TraceID: span.TraceID.String(),
},
}
if len(info.Transaction.Name) == 0 {
// Name is required by Relay so use the operation name if the span name is empty.
info.Transaction.Name = span.Op
}
if result.callerGoID > 0 {
info.Transaction.ActiveThreadID = result.callerGoID
}
return info
}
func (info *profileInfo) UpdateFromEvent(event *Event) {
info.Environment = event.Environment
info.Platform = event.Platform
info.Release = event.Release
info.Dist = event.Dist
info.Transaction.ID = event.EventID
getStringFromContext := func(context map[string]interface{}, originalValue, key string) string {
v, ok := context[key]
if !ok {
return originalValue
}
if s, ok := v.(string); ok {
return s
}
return originalValue
}
if runtimeContext, ok := event.Contexts["runtime"]; ok {
info.Runtime.Name = getStringFromContext(runtimeContext, info.Runtime.Name, "name")
info.Runtime.Version = getStringFromContext(runtimeContext, info.Runtime.Version, "version")
}
if osContext, ok := event.Contexts["os"]; ok {
info.OS.Name = getStringFromContext(osContext, info.OS.Name, "name")
}
if deviceContext, ok := event.Contexts["device"]; ok {
info.Device.Architecture = getStringFromContext(deviceContext, info.Device.Architecture, "arch")
}
}

View File

@@ -18,6 +18,20 @@ const (
SentryBaggageHeader = "baggage"
)
// SpanOrigin indicates what created a trace or a span. See: https://develop.sentry.dev/sdk/performance/trace-origin/
type SpanOrigin string
const (
SpanOriginManual = "manual"
SpanOriginEcho = "auto.http.echo"
SpanOriginFastHTTP = "auto.http.fasthttp"
SpanOriginFiber = "auto.http.fiber"
SpanOriginGin = "auto.http.gin"
SpanOriginStdLib = "auto.http.stdlib"
SpanOriginIris = "auto.http.iris"
SpanOriginNegroni = "auto.http.negroni"
)
// A Span is the building block of a Sentry transaction. Spans build up a tree
// structure of timed operations. The span tree makes up a transaction event
// that is sent to Sentry when the root span is finished.
@@ -37,6 +51,7 @@ type Span struct { //nolint: maligned // prefer readability over optimal memory
Data map[string]interface{} `json:"data,omitempty"`
Sampled Sampled `json:"-"`
Source TransactionSource `json:"-"`
Origin SpanOrigin `json:"origin,omitempty"`
// mu protects concurrent writes to map fields
mu sync.RWMutex
@@ -53,8 +68,6 @@ type Span struct { //nolint: maligned // prefer readability over optimal memory
recorder *spanRecorder
// span context, can only be set on transactions
contexts map[string]Context
// collectProfile is a function that collects a profile of the current transaction. May be nil.
collectProfile transactionProfiler
// a Once instance to make sure that Finish() is only called once.
finishOnce sync.Once
}
@@ -113,11 +126,19 @@ func StartSpan(ctx context.Context, operation string, options ...SpanOption) *Sp
parent: parent,
}
_, err := rand.Read(span.SpanID[:])
if err != nil {
panic(err)
}
if hasParent {
span.TraceID = parent.TraceID
span.ParentSpanID = parent.SpanID
span.Origin = parent.Origin
} else {
// Only set the Source if this is a transaction
span.Source = SourceCustom
span.Origin = SpanOriginManual
// Implementation note:
//
@@ -154,13 +175,6 @@ func StartSpan(ctx context.Context, operation string, options ...SpanOption) *Sp
panic(err)
}
}
_, err := rand.Read(span.SpanID[:])
if err != nil {
panic(err)
}
if hasParent {
span.ParentSpanID = parent.SpanID
}
// Apply options to override defaults.
for _, option := range options {
@@ -176,15 +190,14 @@ func StartSpan(ctx context.Context, operation string, options ...SpanOption) *Sp
span.recorder.record(&span)
hub := hubFromContext(ctx)
// Update scope so that all events include a trace context, allowing
// Sentry to correlate errors to transactions/spans.
hub.Scope().SetContext("trace", span.traceContext().Map())
// Start profiling only if it's a sampled root transaction.
if span.IsTransaction() && span.Sampled.Bool() {
span.sampleTransactionProfile()
clientOptions := span.clientOptions()
if clientOptions.EnableTracing {
hub := hubFromContext(ctx)
if !span.IsTransaction() {
// Push a new scope to stack for non transaction span
hub.PushScope()
}
hub.Scope().SetSpan(&span)
}
return &span
@@ -308,17 +321,21 @@ func (s *Span) ToSentryTrace() string {
// Use this function to propagate the DynamicSamplingContext to a downstream SDK,
// either as the value of the "baggage" HTTP header, or as an html "baggage" meta tag.
func (s *Span) ToBaggage() string {
if containingTransaction := s.GetTransaction(); containingTransaction != nil {
// In case there is currently no frozen DynamicSamplingContext attached to the transaction,
// create one from the properties of the transaction.
if !s.dynamicSamplingContext.IsFrozen() {
// This will return a frozen DynamicSamplingContext.
s.dynamicSamplingContext = DynamicSamplingContextFromTransaction(containingTransaction)
}
return containingTransaction.dynamicSamplingContext.String()
t := s.GetTransaction()
if t == nil {
return ""
}
return ""
// In case there is currently no frozen DynamicSamplingContext attached to the transaction,
// create one from the properties of the transaction.
if !s.dynamicSamplingContext.IsFrozen() {
// This will return a frozen DynamicSamplingContext.
if dsc := DynamicSamplingContextFromTransaction(t); dsc.HasEntries() {
t.dynamicSamplingContext = dsc
}
}
return t.dynamicSamplingContext.String()
}
// SetDynamicSamplingContext sets the given dynamic sampling context on the
@@ -335,6 +352,12 @@ func (s *Span) doFinish() {
s.EndTime = monotonicTimeSince(s.StartTime)
}
hub := hubFromContext(s.ctx)
if !s.IsTransaction() {
// Referenced to StartSpan function that pushes current non-transaction span to scope stack
defer hub.PopScope()
}
if !s.Sampled.Bool() {
return
}
@@ -343,14 +366,9 @@ func (s *Span) doFinish() {
return
}
if s.collectProfile != nil {
event.sdkMetaData.transactionProfile = s.collectProfile(s)
}
// TODO(tracing): add breadcrumbs
// (see https://github.com/getsentry/sentry-python/blob/f6f3525f8812f609/sentry_sdk/tracing.py#L372)
hub := hubFromContext(s.ctx)
hub.CaptureEvent(event)
}
@@ -534,7 +552,7 @@ func (s *Span) toEvent() *Event {
s.dynamicSamplingContext = DynamicSamplingContextFromTransaction(s)
}
contexts := map[string]Context{}
contexts := make(map[string]Context, len(s.contexts)+1)
for k, v := range s.contexts {
contexts[k] = cloneContext(v)
}
@@ -706,31 +724,32 @@ const (
maxSpanStatus
)
var spanStatuses = [maxSpanStatus]string{
"",
"ok",
"cancelled", // [sic]
"unknown",
"invalid_argument",
"deadline_exceeded",
"not_found",
"already_exists",
"permission_denied",
"resource_exhausted",
"failed_precondition",
"aborted",
"out_of_range",
"unimplemented",
"internal_error",
"unavailable",
"data_loss",
"unauthenticated",
}
func (ss SpanStatus) String() string {
if ss >= maxSpanStatus {
return ""
}
m := [maxSpanStatus]string{
"",
"ok",
"cancelled", // [sic]
"unknown",
"invalid_argument",
"deadline_exceeded",
"not_found",
"already_exists",
"permission_denied",
"resource_exhausted",
"failed_precondition",
"aborted",
"out_of_range",
"unimplemented",
"internal_error",
"unavailable",
"data_loss",
"unauthenticated",
}
return m[ss]
return spanStatuses[ss]
}
func (ss SpanStatus) MarshalJSON() ([]byte, error) {
@@ -870,6 +889,25 @@ func WithSpanSampled(sampled Sampled) SpanOption {
}
}
// WithSpanOrigin sets the origin of the span.
func WithSpanOrigin(origin SpanOrigin) SpanOption {
return func(s *Span) {
s.Origin = origin
}
}
// Continue a trace based on traceparent and bagge values.
// If the SDK is configured with tracing enabled,
// this function returns populated SpanOption.
// In any other cases, it populates the propagation context on the scope.
func ContinueTrace(hub *Hub, traceparent, baggage string) SpanOption {
scope := hub.Scope()
propagationContext, _ := PropagationContextFromHeaders(traceparent, baggage)
scope.SetPropagationContext(propagationContext)
return ContinueFromHeaders(traceparent, baggage)
}
// ContinueFromRequest returns a span option that updates the span to continue
// an existing trace. If it cannot detect an existing trace in the request, the
// span will be left unchanged.
@@ -939,6 +977,7 @@ func SpanFromContext(ctx context.Context) *Span {
func StartTransaction(ctx context.Context, name string, options ...SpanOption) *Span {
currentTransaction, exists := ctx.Value(spanContextKey{}).(*Span)
if exists {
currentTransaction.ctx = ctx
return currentTransaction
}

View File

@@ -35,6 +35,7 @@ type Transport interface {
Flush(timeout time.Duration) bool
Configure(options ClientOptions)
SendEvent(event *Event)
Close()
}
func getProxyConfig(options ClientOptions) func(*http.Request) (*url.URL, error) {
@@ -95,55 +96,6 @@ func getRequestBodyFromEvent(event *Event) []byte {
return nil
}
func marshalMetrics(metrics []Metric) []byte {
var b bytes.Buffer
for i, metric := range metrics {
b.WriteString(metric.GetKey())
if unit := metric.GetUnit(); unit != "" {
b.WriteString(fmt.Sprintf("@%s", unit))
}
b.WriteString(fmt.Sprintf("%s|%s", metric.SerializeValue(), metric.GetType()))
if serializedTags := metric.SerializeTags(); serializedTags != "" {
b.WriteString(fmt.Sprintf("|#%s", serializedTags))
}
b.WriteString(fmt.Sprintf("|T%d", metric.GetTimestamp()))
if i < len(metrics)-1 {
b.WriteString("\n")
}
}
return b.Bytes()
}
func encodeMetric(enc *json.Encoder, b io.Writer, metrics []Metric) error {
body := marshalMetrics(metrics)
// Item header
err := enc.Encode(struct {
Type string `json:"type"`
Length int `json:"length"`
}{
Type: metricType,
Length: len(body),
})
if err != nil {
return err
}
// metric payload
if _, err = b.Write(body); 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 err
}
func encodeAttachment(enc *json.Encoder, b io.Writer, attachment *Attachment) error {
// Attachment header
err := enc.Encode(struct {
@@ -228,8 +180,6 @@ func envelopeFromBody(event *Event, dsn *Dsn, sentAt time.Time, body json.RawMes
switch event.Type {
case transactionType, checkInType:
err = encodeEnvelopeItem(enc, event.Type, body)
case metricType:
err = encodeMetric(enc, &b, event.Metrics)
default:
err = encodeEnvelopeItem(enc, eventType, body)
}
@@ -245,18 +195,6 @@ func envelopeFromBody(event *Event, dsn *Dsn, sentAt time.Time, body json.RawMes
}
}
// Profile data
if event.sdkMetaData.transactionProfile != nil {
body, err = json.Marshal(event.sdkMetaData.transactionProfile)
if err != nil {
return nil, err
}
err = encodeEnvelopeItem(enc, profileType, body)
if err != nil {
return nil, err
}
}
return &b, nil
}
@@ -279,10 +217,12 @@ func getRequestFromEvent(ctx context.Context, event *Event, dsn *Dsn) (r *http.R
r.Header.Set("X-Sentry-Auth", auth)
}
}()
body := getRequestBodyFromEvent(event)
if body == nil {
return nil, errors.New("event could not be marshaled")
}
envelope, err := envelopeFromBody(event, dsn, time.Now(), body)
if err != nil {
return nil, err
@@ -350,6 +290,9 @@ type HTTPTransport struct {
mu sync.RWMutex
limits ratelimit.Map
// receiving signal will terminate worker.
done chan struct{}
}
// NewHTTPTransport returns a new pre-configured instance of HTTPTransport.
@@ -357,7 +300,7 @@ func NewHTTPTransport() *HTTPTransport {
transport := HTTPTransport{
BufferSize: defaultBufferSize,
Timeout: defaultTimeout,
limits: make(ratelimit.Map),
done: make(chan struct{}),
}
return &transport
}
@@ -523,6 +466,15 @@ fail:
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() {
close(t.done)
}
func (t *HTTPTransport) worker() {
for b := range t.buffer {
// Signal that processing of the current batch has started.
@@ -533,30 +485,44 @@ func (t *HTTPTransport) worker() {
t.buffer <- b
// Process all batch items.
for item := range b.items {
if t.disabled(item.category) {
continue
}
response, err := t.client.Do(item.request)
if err != nil {
Logger.Printf("There was an issue with sending an event: %v", err)
continue
}
if response.StatusCode >= 400 && response.StatusCode <= 599 {
b, err := io.ReadAll(response.Body)
if err != nil {
Logger.Printf("Error while reading response code: %v", err)
loop:
for {
select {
case <-t.done:
return
case item, open := <-b.items:
if !open {
break loop
}
Logger.Printf("Sending %s failed with the following error: %s", eventType, string(b))
if t.disabled(item.category) {
continue
}
response, err := t.client.Do(item.request)
if err != nil {
Logger.Printf("There was an issue with sending an event: %v", err)
continue
}
if response.StatusCode >= 400 && response.StatusCode <= 599 {
b, err := io.ReadAll(response.Body)
if err != nil {
Logger.Printf("Error while reading response code: %v", err)
}
Logger.Printf("Sending %s failed with the following error: %s", eventType, string(b))
}
t.mu.Lock()
if t.limits == nil {
t.limits = make(ratelimit.Map)
}
t.limits.Merge(ratelimit.FromResponse(response))
t.mu.Unlock()
// Drain body up to a limit and close it, allowing the
// transport to reuse TCP connections.
_, _ = io.CopyN(io.Discard, response.Body, maxDrainResponseBytes)
response.Body.Close()
}
t.mu.Lock()
t.limits.Merge(ratelimit.FromResponse(response))
t.mu.Unlock()
// Drain body up to a limit and close it, allowing the
// transport to reuse TCP connections.
_, _ = io.CopyN(io.Discard, response.Body, maxDrainResponseBytes)
response.Body.Close()
}
// Signal that processing of the batch is done.
@@ -644,6 +610,8 @@ 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 {
@@ -663,8 +631,6 @@ func (t *HTTPSyncTransport) SendEventWithContext(ctx context.Context, event *Eve
switch {
case event.Type == transactionType:
eventType = "transaction"
case event.Type == metricType:
eventType = metricType
default:
eventType = fmt.Sprintf("%s event", event.Level)
}
@@ -690,6 +656,10 @@ func (t *HTTPSyncTransport) SendEventWithContext(ctx context.Context, event *Eve
}
t.mu.Lock()
if t.limits == nil {
t.limits = make(ratelimit.Map)
}
t.limits.Merge(ratelimit.FromResponse(response))
t.mu.Unlock()
@@ -735,3 +705,5 @@ func (noopTransport) SendEvent(*Event) {
func (noopTransport) Flush(time.Duration) bool {
return true
}
func (noopTransport) Close() {}