Initial commit
This commit is contained in:
15
vendor/github.com/getsentry/sentry-go/.codecov.yml
generated
vendored
Normal file
15
vendor/github.com/getsentry/sentry-go/.codecov.yml
generated
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
codecov:
|
||||
# across
|
||||
notify:
|
||||
# Do not notify until at least this number of reports have been uploaded
|
||||
# from the CI pipeline. We normally have more than that number, but 6
|
||||
# should be enough to get a first notification.
|
||||
after_n_builds: 6
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
# Do not fail the commit status if the coverage was reduced up to this value
|
||||
threshold: 0.5%
|
||||
ignore:
|
||||
- "log_fallback.go"
|
||||
40
vendor/github.com/getsentry/sentry-go/.craft.yml
generated
vendored
Normal file
40
vendor/github.com/getsentry/sentry-go/.craft.yml
generated
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
minVersion: 0.35.0
|
||||
changelogPolicy: simple
|
||||
artifactProvider:
|
||||
name: none
|
||||
targets:
|
||||
- name: github
|
||||
tagPrefix: v
|
||||
- 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:
|
||||
5
vendor/github.com/getsentry/sentry-go/.gitattributes
generated
vendored
Normal file
5
vendor/github.com/getsentry/sentry-go/.gitattributes
generated
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
# Tell Git to use LF for line endings on all platforms.
|
||||
# Required to have correct test data on Windows.
|
||||
# https://github.com/mvdan/github-actions-golang#caveats
|
||||
# https://github.com/actions/checkout/issues/135#issuecomment-613361104
|
||||
* text eol=lf
|
||||
14
vendor/github.com/getsentry/sentry-go/.gitignore
generated
vendored
Normal file
14
vendor/github.com/getsentry/sentry-go/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
# Code coverage artifacts
|
||||
coverage.txt
|
||||
coverage.out
|
||||
coverage.html
|
||||
.coverage/
|
||||
|
||||
# Just my personal way of tracking stuff — Kamil
|
||||
FIXME.md
|
||||
TODO.md
|
||||
!NOTES.md
|
||||
|
||||
# IDE system files
|
||||
.idea
|
||||
.vscode
|
||||
62
vendor/github.com/getsentry/sentry-go/.golangci.yml
generated
vendored
Normal file
62
vendor/github.com/getsentry/sentry-go/.golangci.yml
generated
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
version: "2"
|
||||
linters:
|
||||
default: none
|
||||
enable:
|
||||
- bodyclose
|
||||
- dogsled
|
||||
- dupl
|
||||
- errcheck
|
||||
- gochecknoinits
|
||||
- goconst
|
||||
- gocritic
|
||||
- gocyclo
|
||||
- godot
|
||||
- gosec
|
||||
- govet
|
||||
- ineffassign
|
||||
- misspell
|
||||
- nakedret
|
||||
- prealloc
|
||||
- revive
|
||||
- staticcheck
|
||||
- unconvert
|
||||
- unparam
|
||||
- unused
|
||||
- whitespace
|
||||
exclusions:
|
||||
generated: lax
|
||||
presets:
|
||||
- comments
|
||||
- common-false-positives
|
||||
- legacy
|
||||
- std-error-handling
|
||||
rules:
|
||||
- linters:
|
||||
- goconst
|
||||
- prealloc
|
||||
path: _test\.go
|
||||
- linters:
|
||||
- gosec
|
||||
path: _test\.go
|
||||
text: 'G306:'
|
||||
- linters:
|
||||
- unused
|
||||
path: errors_test\.go
|
||||
- linters:
|
||||
- bodyclose
|
||||
- errcheck
|
||||
path: http/example_test\.go
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
formatters:
|
||||
enable:
|
||||
- gofmt
|
||||
- goimports
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
1013
vendor/github.com/getsentry/sentry-go/CHANGELOG.md
generated
vendored
Normal file
1013
vendor/github.com/getsentry/sentry-go/CHANGELOG.md
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
98
vendor/github.com/getsentry/sentry-go/CONTRIBUTING.md
generated
vendored
Normal file
98
vendor/github.com/getsentry/sentry-go/CONTRIBUTING.md
generated
vendored
Normal file
@@ -0,0 +1,98 @@
|
||||
# Contributing to sentry-go
|
||||
|
||||
Hey, thank you if you're reading this, we welcome your contribution!
|
||||
|
||||
## Sending a Pull Request
|
||||
|
||||
Please help us save time when reviewing your PR by following this simple
|
||||
process:
|
||||
|
||||
1. Is your PR a simple typo fix? Read no further, **click that green "Create
|
||||
pull request" button**!
|
||||
|
||||
2. For more complex PRs that involve behavior changes or new APIs, please
|
||||
consider [opening an **issue**][new-issue] describing the problem you're
|
||||
trying to solve if there's not one already.
|
||||
|
||||
A PR is often one specific solution to a problem and sometimes talking about
|
||||
the problem unfolds new possible solutions. Remember we will be responsible
|
||||
for maintaining the changes later.
|
||||
|
||||
3. Fixing a bug and changing a behavior? Please add automated tests to prevent
|
||||
future regression.
|
||||
|
||||
4. Practice writing good commit messages. We have [commit
|
||||
guidelines][commit-guide].
|
||||
|
||||
5. We have [guidelines for PR submitters][pr-guide]. A short summary:
|
||||
|
||||
- Good PR descriptions are very helpful and most of the time they include
|
||||
**why** something is done and why done in this particular way. Also list
|
||||
other possible solutions that were considered and discarded.
|
||||
- Be your own first reviewer. Make sure your code compiles and passes the
|
||||
existing tests.
|
||||
|
||||
[new-issue]: https://github.com/getsentry/sentry-go/issues/new/choose
|
||||
[commit-guide]: https://develop.sentry.dev/code-review/#commit-guidelines
|
||||
[pr-guide]: https://develop.sentry.dev/code-review/#guidelines-for-submitters
|
||||
|
||||
Please also read through our [SDK Development docs](https://develop.sentry.dev/sdk/).
|
||||
It contains information about SDK features, expected payloads and best practices for
|
||||
contributing to Sentry SDKs.
|
||||
|
||||
## Community
|
||||
|
||||
The public-facing channels for support and development of Sentry SDKs can be found on [Discord](https://discord.gg/Ww9hbqr).
|
||||
|
||||
## Testing
|
||||
|
||||
```console
|
||||
$ go test
|
||||
```
|
||||
|
||||
### Watch mode
|
||||
|
||||
Use: https://github.com/cespare/reflex
|
||||
|
||||
```console
|
||||
$ reflex -g '*.go' -d "none" -- sh -c 'printf "\n"; go test'
|
||||
```
|
||||
|
||||
### With data race detection
|
||||
|
||||
```console
|
||||
$ go test -race
|
||||
```
|
||||
|
||||
### Coverage
|
||||
|
||||
```console
|
||||
$ go test -race -coverprofile=coverage.txt -covermode=atomic && go tool cover -html coverage.txt
|
||||
```
|
||||
|
||||
## Linting
|
||||
|
||||
Lint with [`golangci-lint`](https://github.com/golangci/golangci-lint):
|
||||
|
||||
```console
|
||||
$ golangci-lint run
|
||||
```
|
||||
|
||||
## Release
|
||||
|
||||
1. Update `CHANGELOG.md` with new version in `vX.X.X` format title and list of changes.
|
||||
|
||||
The command below can be used to get a list of changes since the last tag, with the format used in `CHANGELOG.md`:
|
||||
|
||||
```console
|
||||
$ git log --no-merges --format=%s $(git describe --abbrev=0).. | sed 's/^/- /'
|
||||
```
|
||||
|
||||
2. Commit with `misc: vX.X.X changelog` commit message and push to `master`.
|
||||
|
||||
3. Let [`craft`](https://github.com/getsentry/craft) do the rest:
|
||||
|
||||
```console
|
||||
$ craft prepare X.X.X
|
||||
$ craft publish X.X.X
|
||||
```
|
||||
21
vendor/github.com/getsentry/sentry-go/LICENSE
generated
vendored
Normal file
21
vendor/github.com/getsentry/sentry-go/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Functional Software, Inc. dba Sentry
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
3
vendor/github.com/getsentry/sentry-go/MIGRATION.md
generated
vendored
Normal file
3
vendor/github.com/getsentry/sentry-go/MIGRATION.md
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# `raven-go` to `sentry-go` Migration Guide
|
||||
|
||||
A [`raven-go` to `sentry-go` migration guide](https://docs.sentry.io/platforms/go/migration/) is available at the official Sentry documentation site.
|
||||
82
vendor/github.com/getsentry/sentry-go/Makefile
generated
vendored
Normal file
82
vendor/github.com/getsentry/sentry-go/Makefile
generated
vendored
Normal file
@@ -0,0 +1,82 @@
|
||||
.DEFAULT_GOAL := help
|
||||
|
||||
MKFILE_PATH := $(abspath $(lastword $(MAKEFILE_LIST)))
|
||||
MKFILE_DIR := $(dir $(MKFILE_PATH))
|
||||
ALL_GO_MOD_DIRS := $(shell find . -type f -name 'go.mod' -exec dirname {} \; | sort)
|
||||
GO = go
|
||||
TIMEOUT = 300
|
||||
|
||||
# Parse Makefile and display the help
|
||||
help: ## Show help
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
|
||||
.PHONY: help
|
||||
|
||||
build: ## Build everything
|
||||
for dir in $(ALL_GO_MOD_DIRS); do \
|
||||
cd "$${dir}"; \
|
||||
echo ">>> Running 'go build' for module: $${dir}"; \
|
||||
go build ./...; \
|
||||
done;
|
||||
.PHONY: build
|
||||
|
||||
### Tests (inspired by https://github.com/open-telemetry/opentelemetry-go/blob/main/Makefile)
|
||||
TEST_TARGETS := test-short test-verbose test-race
|
||||
test-race: ARGS=-race
|
||||
test-short: ARGS=-short
|
||||
test-verbose: ARGS=-v -race
|
||||
$(TEST_TARGETS): test
|
||||
test: $(ALL_GO_MOD_DIRS:%=test/%) ## Run tests
|
||||
test/%: DIR=$*
|
||||
test/%:
|
||||
@echo ">>> Running tests for module: $(DIR)"
|
||||
@# We use '-count=1' to disable test caching.
|
||||
(cd $(DIR) && $(GO) test -count=1 -timeout $(TIMEOUT)s $(ARGS) ./...)
|
||||
.PHONY: $(TEST_TARGETS) test
|
||||
|
||||
# Coverage
|
||||
COVERAGE_MODE = atomic
|
||||
COVERAGE_PROFILE = coverage.out
|
||||
COVERAGE_REPORT_DIR = .coverage
|
||||
COVERAGE_REPORT_DIR_ABS = "$(MKFILE_DIR)/$(COVERAGE_REPORT_DIR)"
|
||||
$(COVERAGE_REPORT_DIR):
|
||||
mkdir -p $(COVERAGE_REPORT_DIR)
|
||||
clean-report-dir: $(COVERAGE_REPORT_DIR)
|
||||
test $(COVERAGE_REPORT_DIR) && rm -f $(COVERAGE_REPORT_DIR)/*
|
||||
test-coverage: $(COVERAGE_REPORT_DIR) clean-report-dir ## Test with coverage enabled
|
||||
set -e ; \
|
||||
for dir in $(ALL_GO_MOD_DIRS); do \
|
||||
echo ">>> Running tests with coverage for module: $${dir}"; \
|
||||
DIR_ABS=$$(python -c 'import os, sys; print(os.path.realpath(sys.argv[1]))' $${dir}) ; \
|
||||
REPORT_NAME=$$(basename $${DIR_ABS}); \
|
||||
(cd "$${dir}" && \
|
||||
$(GO) test -count=1 -timeout $(TIMEOUT)s -coverpkg=./... -covermode=$(COVERAGE_MODE) -coverprofile="$(COVERAGE_PROFILE)" ./... && \
|
||||
cp $(COVERAGE_PROFILE) "$(COVERAGE_REPORT_DIR_ABS)/$${REPORT_NAME}_$(COVERAGE_PROFILE)" && \
|
||||
$(GO) tool cover -html=$(COVERAGE_PROFILE) -o coverage.html); \
|
||||
done;
|
||||
.PHONY: test-coverage clean-report-dir
|
||||
|
||||
mod-tidy: ## Check go.mod tidiness
|
||||
set -e ; \
|
||||
for dir in $(ALL_GO_MOD_DIRS); do \
|
||||
echo ">>> Running 'go mod tidy' for module: $${dir}"; \
|
||||
(cd "$${dir}" && go mod tidy -go=1.21 -compat=1.21); \
|
||||
done; \
|
||||
git diff --exit-code;
|
||||
.PHONY: mod-tidy
|
||||
|
||||
vet: ## Run "go vet"
|
||||
set -e ; \
|
||||
for dir in $(ALL_GO_MOD_DIRS); do \
|
||||
echo ">>> Running 'go vet' for module: $${dir}"; \
|
||||
(cd "$${dir}" && go vet ./...); \
|
||||
done;
|
||||
.PHONY: vet
|
||||
|
||||
|
||||
lint: ## Lint (using "golangci-lint")
|
||||
golangci-lint run
|
||||
.PHONY: lint
|
||||
|
||||
fmt: ## Format all Go files
|
||||
gofmt -l -w -s .
|
||||
.PHONY: fmt
|
||||
106
vendor/github.com/getsentry/sentry-go/README.md
generated
vendored
Normal file
106
vendor/github.com/getsentry/sentry-go/README.md
generated
vendored
Normal file
@@ -0,0 +1,106 @@
|
||||
<p align="center">
|
||||
<a href="https://sentry.io/?utm_source=github&utm_medium=logo" target="_blank">
|
||||
<picture>
|
||||
<source srcset="https://sentry-brand.storage.googleapis.com/sentry-logo-white.png" media="(prefers-color-scheme: dark)" />
|
||||
<source srcset="https://sentry-brand.storage.googleapis.com/sentry-logo-black.png" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" />
|
||||
<img src="https://sentry-brand.storage.googleapis.com/sentry-logo-black.png" alt="Sentry" width="280">
|
||||
</picture>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
# Official Sentry SDK for Go
|
||||
|
||||
[](https://github.com/getsentry/sentry-go/actions/workflows/test.yml)
|
||||
[](https://goreportcard.com/report/github.com/getsentry/sentry-go)
|
||||
[](https://discord.gg/Ww9hbqr)
|
||||
[](https://pkg.go.dev/github.com/getsentry/sentry-go)
|
||||
|
||||
`sentry-go` provides a Sentry client implementation for the Go programming
|
||||
language. This is the next generation of the Go SDK for [Sentry](https://sentry.io/),
|
||||
intended to replace the `raven-go` package.
|
||||
|
||||
> Looking for the old `raven-go` SDK documentation? See the Legacy client section [here](https://docs.sentry.io/clients/go/).
|
||||
> If you want to start using `sentry-go` instead, check out the [migration guide](https://docs.sentry.io/platforms/go/migration/).
|
||||
|
||||
## Requirements
|
||||
|
||||
The only requirement is a Go compiler.
|
||||
|
||||
We verify this package against the 3 most recent releases of Go. Those are the
|
||||
supported versions. The exact versions are defined in
|
||||
[`GitHub workflow`](.github/workflows/test.yml).
|
||||
|
||||
In addition, we run tests against the current master branch of the Go toolchain,
|
||||
though support for this configuration is best-effort.
|
||||
|
||||
## Installation
|
||||
|
||||
`sentry-go` can be installed like any other Go library through `go get`:
|
||||
|
||||
```console
|
||||
$ go get github.com/getsentry/sentry-go@latest
|
||||
```
|
||||
|
||||
Check out the [list of released versions](https://github.com/getsentry/sentry-go/releases).
|
||||
|
||||
## Configuration
|
||||
|
||||
To use `sentry-go`, you’ll need to import the `sentry-go` package and initialize
|
||||
it with your DSN and other [options](https://pkg.go.dev/github.com/getsentry/sentry-go#ClientOptions).
|
||||
|
||||
If not specified in the SDK initialization, the
|
||||
[DSN](https://docs.sentry.io/product/sentry-basics/dsn-explainer/),
|
||||
[Release](https://docs.sentry.io/product/releases/) and
|
||||
[Environment](https://docs.sentry.io/product/sentry-basics/environments/)
|
||||
are read from the environment variables `SENTRY_DSN`, `SENTRY_RELEASE` and
|
||||
`SENTRY_ENVIRONMENT`, respectively.
|
||||
|
||||
More on this in the [Configuration section of the official Sentry Go SDK documentation](https://docs.sentry.io/platforms/go/configuration/).
|
||||
|
||||
## Usage
|
||||
|
||||
The SDK supports reporting errors and tracking application performance.
|
||||
|
||||
To get started, have a look at one of our [examples](_examples/):
|
||||
- [Basic error instrumentation](_examples/basic/main.go)
|
||||
- [Error and tracing for HTTP servers](_examples/http/main.go)
|
||||
|
||||
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`,
|
||||
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/)
|
||||
- [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
|
||||
|
||||
- [Bug Tracker](https://github.com/getsentry/sentry-go/issues)
|
||||
- [GitHub Project](https://github.com/getsentry/sentry-go)
|
||||
- [](https://pkg.go.dev/github.com/getsentry/sentry-go)
|
||||
- [](https://docs.sentry.io/platforms/go/)
|
||||
- [](https://github.com/getsentry/sentry-go/discussions)
|
||||
- [](https://discord.gg/Ww9hbqr)
|
||||
- [](http://stackoverflow.com/questions/tagged/sentry)
|
||||
- [](https://twitter.com/intent/follow?screen_name=getsentry)
|
||||
|
||||
## License
|
||||
|
||||
Licensed under
|
||||
[The MIT License](https://opensource.org/licenses/mit/), see
|
||||
[`LICENSE`](LICENSE).
|
||||
|
||||
## Community
|
||||
|
||||
Join Sentry's [`#go` channel on Discord](https://discord.gg/Ww9hbqr) to get
|
||||
involved and help us improve the SDK!
|
||||
36
vendor/github.com/getsentry/sentry-go/attribute/bulider.go
generated
vendored
Normal file
36
vendor/github.com/getsentry/sentry-go/attribute/bulider.go
generated
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
package attribute
|
||||
|
||||
type Builder struct {
|
||||
Key string
|
||||
Value Value
|
||||
}
|
||||
|
||||
// String returns a Builder for a string value.
|
||||
func String(key, value string) Builder {
|
||||
return Builder{key, StringValue(value)}
|
||||
}
|
||||
|
||||
// Int64 returns a Builder for an int64.
|
||||
func Int64(key string, value int64) Builder {
|
||||
return Builder{key, Int64Value(value)}
|
||||
}
|
||||
|
||||
// Int returns a Builder for an int64.
|
||||
func Int(key string, value int) Builder {
|
||||
return Builder{key, IntValue(value)}
|
||||
}
|
||||
|
||||
// Float64 returns a Builder for a float64.
|
||||
func Float64(key string, v float64) Builder {
|
||||
return Builder{key, Float64Value(v)}
|
||||
}
|
||||
|
||||
// Bool returns a Builder for a boolean.
|
||||
func Bool(key string, v bool) Builder {
|
||||
return Builder{key, BoolValue(v)}
|
||||
}
|
||||
|
||||
// Valid checks for valid key and type.
|
||||
func (b *Builder) Valid() bool {
|
||||
return len(b.Key) > 0 && b.Value.Type() != INVALID
|
||||
}
|
||||
49
vendor/github.com/getsentry/sentry-go/attribute/rawhelpers.go
generated
vendored
Normal file
49
vendor/github.com/getsentry/sentry-go/attribute/rawhelpers.go
generated
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
// Copied from https://github.com/open-telemetry/opentelemetry-go/blob/cc43e01c27892252aac9a8f20da28cdde957a289/attribute/rawhelpers.go
|
||||
// Copyright The OpenTelemetry Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package attribute
|
||||
|
||||
import (
|
||||
"math"
|
||||
)
|
||||
|
||||
func boolToRaw(b bool) uint64 { // b is not a control flag.
|
||||
if b {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func rawToBool(r uint64) bool {
|
||||
return r != 0
|
||||
}
|
||||
|
||||
func int64ToRaw(i int64) uint64 {
|
||||
// Assumes original was a valid int64 (overflow not checked).
|
||||
return uint64(i) // nolint: gosec
|
||||
}
|
||||
|
||||
func rawToInt64(r uint64) int64 {
|
||||
// Assumes original was a valid int64 (overflow not checked).
|
||||
return int64(r) // nolint: gosec
|
||||
}
|
||||
|
||||
func float64ToRaw(f float64) uint64 {
|
||||
return math.Float64bits(f)
|
||||
}
|
||||
|
||||
func rawToFloat64(r uint64) float64 {
|
||||
return math.Float64frombits(r)
|
||||
}
|
||||
170
vendor/github.com/getsentry/sentry-go/attribute/value.go
generated
vendored
Normal file
170
vendor/github.com/getsentry/sentry-go/attribute/value.go
generated
vendored
Normal file
@@ -0,0 +1,170 @@
|
||||
// Adapted from https://github.com/open-telemetry/opentelemetry-go/blob/cc43e01c27892252aac9a8f20da28cdde957a289/attribute/value.go
|
||||
//
|
||||
// Copyright The OpenTelemetry Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package attribute
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Type describes the type of the data Value holds.
|
||||
type Type int // redefines builtin Type.
|
||||
|
||||
// Value represents the value part in key-value pairs.
|
||||
type Value struct {
|
||||
vtype Type
|
||||
numeric uint64
|
||||
stringly string
|
||||
}
|
||||
|
||||
const (
|
||||
// INVALID is used for a Value with no value set.
|
||||
INVALID Type = iota
|
||||
// BOOL is a boolean Type Value.
|
||||
BOOL
|
||||
// INT64 is a 64-bit signed integral Type Value.
|
||||
INT64
|
||||
// FLOAT64 is a 64-bit floating point Type Value.
|
||||
FLOAT64
|
||||
// STRING is a string Type Value.
|
||||
STRING
|
||||
)
|
||||
|
||||
// BoolValue creates a BOOL Value.
|
||||
func BoolValue(v bool) Value {
|
||||
return Value{
|
||||
vtype: BOOL,
|
||||
numeric: boolToRaw(v),
|
||||
}
|
||||
}
|
||||
|
||||
// IntValue creates an INT64 Value.
|
||||
func IntValue(v int) Value {
|
||||
return Int64Value(int64(v))
|
||||
}
|
||||
|
||||
// Int64Value creates an INT64 Value.
|
||||
func Int64Value(v int64) Value {
|
||||
return Value{
|
||||
vtype: INT64,
|
||||
numeric: int64ToRaw(v),
|
||||
}
|
||||
}
|
||||
|
||||
// Float64Value creates a FLOAT64 Value.
|
||||
func Float64Value(v float64) Value {
|
||||
return Value{
|
||||
vtype: FLOAT64,
|
||||
numeric: float64ToRaw(v),
|
||||
}
|
||||
}
|
||||
|
||||
// StringValue creates a STRING Value.
|
||||
func StringValue(v string) Value {
|
||||
return Value{
|
||||
vtype: STRING,
|
||||
stringly: v,
|
||||
}
|
||||
}
|
||||
|
||||
// Type returns a type of the Value.
|
||||
func (v Value) Type() Type {
|
||||
return v.vtype
|
||||
}
|
||||
|
||||
// AsBool returns the bool value. Make sure that the Value's type is
|
||||
// BOOL.
|
||||
func (v Value) AsBool() bool {
|
||||
return rawToBool(v.numeric)
|
||||
}
|
||||
|
||||
// AsInt64 returns the int64 value. Make sure that the Value's type is
|
||||
// INT64.
|
||||
func (v Value) AsInt64() int64 {
|
||||
return rawToInt64(v.numeric)
|
||||
}
|
||||
|
||||
// AsFloat64 returns the float64 value. Make sure that the Value's
|
||||
// type is FLOAT64.
|
||||
func (v Value) AsFloat64() float64 {
|
||||
return rawToFloat64(v.numeric)
|
||||
}
|
||||
|
||||
// AsString returns the string value. Make sure that the Value's type
|
||||
// is STRING.
|
||||
func (v Value) AsString() string {
|
||||
return v.stringly
|
||||
}
|
||||
|
||||
type unknownValueType struct{}
|
||||
|
||||
// AsInterface returns Value's data as interface{}.
|
||||
func (v Value) AsInterface() interface{} {
|
||||
switch v.Type() {
|
||||
case BOOL:
|
||||
return v.AsBool()
|
||||
case INT64:
|
||||
return v.AsInt64()
|
||||
case FLOAT64:
|
||||
return v.AsFloat64()
|
||||
case STRING:
|
||||
return v.stringly
|
||||
}
|
||||
return unknownValueType{}
|
||||
}
|
||||
|
||||
// String returns a string representation of Value's data.
|
||||
func (v Value) String() string {
|
||||
switch v.Type() {
|
||||
case BOOL:
|
||||
return strconv.FormatBool(v.AsBool())
|
||||
case INT64:
|
||||
return strconv.FormatInt(v.AsInt64(), 10)
|
||||
case FLOAT64:
|
||||
return fmt.Sprint(v.AsFloat64())
|
||||
case STRING:
|
||||
return v.stringly
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// MarshalJSON returns the JSON encoding of the Value.
|
||||
func (v Value) MarshalJSON() ([]byte, error) {
|
||||
var jsonVal struct {
|
||||
Type string
|
||||
Value interface{}
|
||||
}
|
||||
jsonVal.Type = v.Type().String()
|
||||
jsonVal.Value = v.AsInterface()
|
||||
return json.Marshal(jsonVal)
|
||||
}
|
||||
|
||||
func (t Type) String() string {
|
||||
switch t {
|
||||
case BOOL:
|
||||
return "bool"
|
||||
case INT64:
|
||||
return "int64"
|
||||
case FLOAT64:
|
||||
return "float64"
|
||||
case STRING:
|
||||
return "string"
|
||||
}
|
||||
return "invalid"
|
||||
}
|
||||
94
vendor/github.com/getsentry/sentry-go/batch_logger.go
generated
vendored
Normal file
94
vendor/github.com/getsentry/sentry-go/batch_logger.go
generated
vendored
Normal file
@@ -0,0 +1,94 @@
|
||||
package sentry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
batchSize = 100
|
||||
batchTimeout = 5 * time.Second
|
||||
)
|
||||
|
||||
type BatchLogger struct {
|
||||
client *Client
|
||||
logCh chan Log
|
||||
cancel context.CancelFunc
|
||||
wg sync.WaitGroup
|
||||
startOnce sync.Once
|
||||
}
|
||||
|
||||
func NewBatchLogger(client *Client) *BatchLogger {
|
||||
return &BatchLogger{
|
||||
client: client,
|
||||
logCh: make(chan Log, batchSize),
|
||||
}
|
||||
}
|
||||
|
||||
func (l *BatchLogger) Start() {
|
||||
l.startOnce.Do(func() {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
l.cancel = cancel
|
||||
l.wg.Add(1)
|
||||
go l.run(ctx)
|
||||
})
|
||||
}
|
||||
|
||||
func (l *BatchLogger) Flush() {
|
||||
if l.cancel != nil {
|
||||
l.cancel()
|
||||
l.wg.Wait()
|
||||
}
|
||||
}
|
||||
|
||||
func (l *BatchLogger) run(ctx context.Context) {
|
||||
defer l.wg.Done()
|
||||
var logs []Log
|
||||
timer := time.NewTimer(batchTimeout)
|
||||
|
||||
for {
|
||||
select {
|
||||
case log := <-l.logCh:
|
||||
logs = append(logs, log)
|
||||
if len(logs) >= batchSize {
|
||||
l.processEvent(logs)
|
||||
logs = nil
|
||||
if !timer.Stop() {
|
||||
<-timer.C
|
||||
}
|
||||
timer.Reset(batchTimeout)
|
||||
}
|
||||
case <-timer.C:
|
||||
if len(logs) > 0 {
|
||||
l.processEvent(logs)
|
||||
logs = nil
|
||||
}
|
||||
timer.Reset(batchTimeout)
|
||||
case <-ctx.Done():
|
||||
// Drain remaining logs from channel
|
||||
drain:
|
||||
for {
|
||||
select {
|
||||
case log := <-l.logCh:
|
||||
logs = append(logs, log)
|
||||
default:
|
||||
break drain
|
||||
}
|
||||
}
|
||||
|
||||
if len(logs) > 0 {
|
||||
l.processEvent(logs)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l *BatchLogger) processEvent(logs []Log) {
|
||||
event := NewEvent()
|
||||
event.Timestamp = time.Now()
|
||||
event.Type = logEvent.Type
|
||||
event.Logs = logs
|
||||
l.client.CaptureEvent(event, nil, nil)
|
||||
}
|
||||
121
vendor/github.com/getsentry/sentry-go/check_in.go
generated
vendored
Normal file
121
vendor/github.com/getsentry/sentry-go/check_in.go
generated
vendored
Normal file
@@ -0,0 +1,121 @@
|
||||
package sentry
|
||||
|
||||
import "time"
|
||||
|
||||
type CheckInStatus string
|
||||
|
||||
const (
|
||||
CheckInStatusInProgress CheckInStatus = "in_progress"
|
||||
CheckInStatusOK CheckInStatus = "ok"
|
||||
CheckInStatusError CheckInStatus = "error"
|
||||
)
|
||||
|
||||
type checkInScheduleType string
|
||||
|
||||
const (
|
||||
checkInScheduleTypeCrontab checkInScheduleType = "crontab"
|
||||
checkInScheduleTypeInterval checkInScheduleType = "interval"
|
||||
)
|
||||
|
||||
type MonitorSchedule interface {
|
||||
// scheduleType is a private method that must be implemented for monitor schedule
|
||||
// implementation. It should never be called. This method is made for having
|
||||
// specific private implementation of MonitorSchedule interface.
|
||||
scheduleType() checkInScheduleType
|
||||
}
|
||||
|
||||
type crontabSchedule struct {
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
func (c crontabSchedule) scheduleType() checkInScheduleType {
|
||||
return checkInScheduleTypeCrontab
|
||||
}
|
||||
|
||||
// CrontabSchedule defines the MonitorSchedule with a cron format.
|
||||
// Example: "8 * * * *".
|
||||
func CrontabSchedule(scheduleString string) MonitorSchedule {
|
||||
return crontabSchedule{
|
||||
Type: string(checkInScheduleTypeCrontab),
|
||||
Value: scheduleString,
|
||||
}
|
||||
}
|
||||
|
||||
type intervalSchedule struct {
|
||||
Type string `json:"type"`
|
||||
Value int64 `json:"value"`
|
||||
Unit string `json:"unit"`
|
||||
}
|
||||
|
||||
func (i intervalSchedule) scheduleType() checkInScheduleType {
|
||||
return checkInScheduleTypeInterval
|
||||
}
|
||||
|
||||
type MonitorScheduleUnit string
|
||||
|
||||
const (
|
||||
MonitorScheduleUnitMinute MonitorScheduleUnit = "minute"
|
||||
MonitorScheduleUnitHour MonitorScheduleUnit = "hour"
|
||||
MonitorScheduleUnitDay MonitorScheduleUnit = "day"
|
||||
MonitorScheduleUnitWeek MonitorScheduleUnit = "week"
|
||||
MonitorScheduleUnitMonth MonitorScheduleUnit = "month"
|
||||
MonitorScheduleUnitYear MonitorScheduleUnit = "year"
|
||||
)
|
||||
|
||||
// IntervalSchedule defines the MonitorSchedule with an interval format.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// IntervalSchedule(1, sentry.MonitorScheduleUnitDay)
|
||||
func IntervalSchedule(value int64, unit MonitorScheduleUnit) MonitorSchedule {
|
||||
return intervalSchedule{
|
||||
Type: string(checkInScheduleTypeInterval),
|
||||
Value: value,
|
||||
Unit: string(unit),
|
||||
}
|
||||
}
|
||||
|
||||
type MonitorConfig struct { //nolint: maligned // prefer readability over optimal memory layout
|
||||
Schedule MonitorSchedule `json:"schedule,omitempty"`
|
||||
// The allowed margin of minutes after the expected check-in time that
|
||||
// the monitor will not be considered missed for.
|
||||
CheckInMargin int64 `json:"checkin_margin,omitempty"`
|
||||
// The allowed duration in minutes that the monitor may be `in_progress`
|
||||
// for before being considered failed due to timeout.
|
||||
MaxRuntime int64 `json:"max_runtime,omitempty"`
|
||||
// A tz database string representing the timezone which the monitor's execution schedule is in.
|
||||
// See: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
||||
Timezone string `json:"timezone,omitempty"`
|
||||
// The number of consecutive failed check-ins it takes before an issue is created.
|
||||
FailureIssueThreshold int64 `json:"failure_issue_threshold,omitempty"`
|
||||
// The number of consecutive OK check-ins it takes before an issue is resolved.
|
||||
RecoveryThreshold int64 `json:"recovery_threshold,omitempty"`
|
||||
}
|
||||
|
||||
type CheckIn struct { //nolint: maligned // prefer readability over optimal memory layout
|
||||
// Check-In ID (unique and client generated)
|
||||
ID EventID `json:"check_in_id"`
|
||||
// The distinct slug of the monitor.
|
||||
MonitorSlug string `json:"monitor_slug"`
|
||||
// The status of the check-in.
|
||||
Status CheckInStatus `json:"status"`
|
||||
// The duration of the check-in. Will only take effect if the status is ok or error.
|
||||
Duration time.Duration `json:"duration,omitempty"`
|
||||
}
|
||||
|
||||
// serializedCheckIn is used by checkInMarshalJSON method on Event struct.
|
||||
// See https://develop.sentry.dev/sdk/check-ins/
|
||||
type serializedCheckIn struct { //nolint: maligned
|
||||
// Check-In ID (unique and client generated).
|
||||
CheckInID string `json:"check_in_id"`
|
||||
// The distinct slug of the monitor.
|
||||
MonitorSlug string `json:"monitor_slug"`
|
||||
// The status of the check-in.
|
||||
Status CheckInStatus `json:"status"`
|
||||
// The duration of the check-in in seconds. Will only take effect if the status is ok or error.
|
||||
Duration float64 `json:"duration,omitempty"`
|
||||
Release string `json:"release,omitempty"`
|
||||
Environment string `json:"environment,omitempty"`
|
||||
MonitorConfig *MonitorConfig `json:"monitor_config,omitempty"`
|
||||
}
|
||||
743
vendor/github.com/getsentry/sentry-go/client.go
generated
vendored
Normal file
743
vendor/github.com/getsentry/sentry-go/client.go
generated
vendored
Normal file
@@ -0,0 +1,743 @@
|
||||
package sentry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/getsentry/sentry-go/internal/debug"
|
||||
)
|
||||
|
||||
// The identifier of the SDK.
|
||||
const sdkIdentifier = "sentry.go"
|
||||
|
||||
// maxErrorDepth is the maximum number of errors reported in a chain of errors.
|
||||
// This protects the SDK from an arbitrarily long chain of wrapped errors.
|
||||
//
|
||||
// An additional consideration is that arguably reporting a long chain of errors
|
||||
// is of little use when debugging production errors with Sentry. The Sentry UI
|
||||
// is not optimized for long chains either. The top-level error together with a
|
||||
// stack trace is often the most useful information.
|
||||
const maxErrorDepth = 10
|
||||
|
||||
// defaultMaxSpans limits the default number of recorded spans per transaction. The limit is
|
||||
// meant to bound memory usage and prevent too large transaction events that
|
||||
// would be rejected by Sentry.
|
||||
const defaultMaxSpans = 1000
|
||||
|
||||
// hostname is the host name reported by the kernel. It is precomputed once to
|
||||
// avoid syscalls when capturing events.
|
||||
//
|
||||
// The error is ignored because retrieving the host name is best-effort. If the
|
||||
// error is non-nil, there is nothing to do other than retrying. We choose not
|
||||
// to retry for now.
|
||||
var hostname, _ = os.Hostname()
|
||||
|
||||
// lockedRand is a random number generator safe for concurrent use. Its API is
|
||||
// intentionally limited and it is not meant as a full replacement for a
|
||||
// rand.Rand.
|
||||
type lockedRand struct {
|
||||
mu sync.Mutex
|
||||
r *rand.Rand
|
||||
}
|
||||
|
||||
// Float64 returns a pseudo-random number in [0.0,1.0).
|
||||
func (r *lockedRand) Float64() float64 {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
return r.r.Float64()
|
||||
}
|
||||
|
||||
// rng is the internal random number generator.
|
||||
//
|
||||
// We do not use the global functions from math/rand because, while they are
|
||||
// safe for concurrent use, any package in a build could change the seed and
|
||||
// affect the generated numbers, for instance making them deterministic. On the
|
||||
// other hand, the source returned from rand.NewSource is not safe for
|
||||
// concurrent use, so we need to couple its use with a sync.Mutex.
|
||||
var rng = &lockedRand{
|
||||
// #nosec G404 -- We are fine using transparent, non-secure value here.
|
||||
r: rand.New(rand.NewSource(time.Now().UnixNano())),
|
||||
}
|
||||
|
||||
// usageError is used to report to Sentry an SDK usage error.
|
||||
//
|
||||
// It is not exported because it is never returned by any function or method in
|
||||
// the exported API.
|
||||
type usageError struct {
|
||||
error
|
||||
}
|
||||
|
||||
// DebugLogger is an instance of log.Logger that is used to provide debug information about running Sentry Client
|
||||
// can be enabled by either using DebugLogger.SetOutput directly or with Debug client option.
|
||||
var DebugLogger = log.New(io.Discard, "[Sentry] ", log.LstdFlags)
|
||||
|
||||
// EventProcessor is a function that processes an event.
|
||||
// Event processors are used to change an event before it is sent to Sentry.
|
||||
type EventProcessor func(event *Event, hint *EventHint) *Event
|
||||
|
||||
// EventModifier is the interface that wraps the ApplyToEvent method.
|
||||
//
|
||||
// ApplyToEvent changes an event based on external data and/or
|
||||
// an event hint.
|
||||
type EventModifier interface {
|
||||
ApplyToEvent(event *Event, hint *EventHint, client *Client) *Event
|
||||
}
|
||||
|
||||
var globalEventProcessors []EventProcessor
|
||||
|
||||
// AddGlobalEventProcessor adds processor to the global list of event
|
||||
// processors. Global event processors apply to all events.
|
||||
//
|
||||
// AddGlobalEventProcessor is deprecated. Most users will prefer to initialize
|
||||
// the SDK with Init and provide a ClientOptions.BeforeSend function or use
|
||||
// Scope.AddEventProcessor instead.
|
||||
func AddGlobalEventProcessor(processor EventProcessor) {
|
||||
globalEventProcessors = append(globalEventProcessors, processor)
|
||||
}
|
||||
|
||||
// Integration allows for registering a functions that modify or discard captured events.
|
||||
type Integration interface {
|
||||
Name() string
|
||||
SetupOnce(client *Client)
|
||||
}
|
||||
|
||||
// ClientOptions that configures a SDK Client.
|
||||
type ClientOptions struct {
|
||||
// The DSN to use. If the DSN is not set, the client is effectively
|
||||
// disabled.
|
||||
Dsn string
|
||||
// In debug mode, the debug information is printed to stdout to help you
|
||||
// understand what sentry is doing.
|
||||
Debug bool
|
||||
// Configures whether SDK should generate and attach stacktraces to pure
|
||||
// capture message calls.
|
||||
AttachStacktrace bool
|
||||
// The sample rate for event submission in the range [0.0, 1.0]. By default,
|
||||
// all events are sent. Thus, as a historical special case, the sample rate
|
||||
// 0.0 is treated as if it was 1.0. To drop all events, set the DSN to the
|
||||
// empty string.
|
||||
SampleRate float64
|
||||
// Enable performance tracing.
|
||||
EnableTracing bool
|
||||
// The sample rate for sampling traces in the range [0.0, 1.0].
|
||||
TracesSampleRate float64
|
||||
// Used to customize the sampling of traces, overrides TracesSampleRate.
|
||||
TracesSampler TracesSampler
|
||||
// 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.
|
||||
IgnoreErrors []string
|
||||
// List of regexp strings that will be used to match against a transaction's
|
||||
// name. If a match is found, then the transaction will be dropped.
|
||||
IgnoreTransactions []string
|
||||
// If this flag is enabled, certain personally identifiable information (PII) is added by active integrations.
|
||||
// By default, no such data is sent.
|
||||
SendDefaultPII bool
|
||||
// BeforeSend is called before error events are sent to Sentry.
|
||||
// You can use it to mutate the event or return nil to discard it.
|
||||
BeforeSend func(event *Event, hint *EventHint) *Event
|
||||
// BeforeSendLong is called before log events are sent to Sentry.
|
||||
// You can use it to mutate the log event or return nil to discard it.
|
||||
BeforeSendLog func(event *Log) *Log
|
||||
// BeforeSendTransaction is called before transaction events are sent to Sentry.
|
||||
// Use it to mutate the transaction or return nil to discard the transaction.
|
||||
BeforeSendTransaction func(event *Event, hint *EventHint) *Event
|
||||
// Before breadcrumb add callback.
|
||||
BeforeBreadcrumb func(breadcrumb *Breadcrumb, hint *BreadcrumbHint) *Breadcrumb
|
||||
// Integrations to be installed on the current Client, receives default
|
||||
// integrations.
|
||||
Integrations func([]Integration) []Integration
|
||||
// io.Writer implementation that should be used with the Debug mode.
|
||||
DebugWriter io.Writer
|
||||
// The transport to use. Defaults to HTTPTransport.
|
||||
Transport Transport
|
||||
// The server name to be reported.
|
||||
ServerName string
|
||||
// The release to be sent with events.
|
||||
//
|
||||
// Some Sentry features are built around releases, and, thus, reporting
|
||||
// events with a non-empty release improves the product experience. See
|
||||
// https://docs.sentry.io/product/releases/.
|
||||
//
|
||||
// If Release is not set, the SDK will try to derive a default value
|
||||
// from environment variables or the Git repository in the working
|
||||
// directory.
|
||||
//
|
||||
// If you distribute a compiled binary, it is recommended to set the
|
||||
// Release value explicitly at build time. As an example, you can use:
|
||||
//
|
||||
// go build -ldflags='-X main.release=VALUE'
|
||||
//
|
||||
// That will set the value of a predeclared variable 'release' in the
|
||||
// 'main' package to 'VALUE'. Then, use that variable when initializing
|
||||
// the SDK:
|
||||
//
|
||||
// sentry.Init(ClientOptions{Release: release})
|
||||
//
|
||||
// See https://golang.org/cmd/go/ and https://golang.org/cmd/link/ for
|
||||
// the official documentation of -ldflags and -X, respectively.
|
||||
Release string
|
||||
// The dist to be sent with events.
|
||||
Dist string
|
||||
// The environment to be sent with events.
|
||||
Environment string
|
||||
// Maximum number of breadcrumbs
|
||||
// when MaxBreadcrumbs is negative then ignore breadcrumbs.
|
||||
MaxBreadcrumbs int
|
||||
// Maximum number of spans.
|
||||
//
|
||||
// See https://develop.sentry.dev/sdk/envelopes/#size-limits for size limits
|
||||
// applied during event ingestion. Events that exceed these limits might get dropped.
|
||||
MaxSpans int
|
||||
// An optional pointer to http.Client that will be used with a default
|
||||
// HTTPTransport. Using your own client will make HTTPTransport, HTTPProxy,
|
||||
// HTTPSProxy and CaCerts options ignored.
|
||||
HTTPClient *http.Client
|
||||
// An optional pointer to http.Transport that will be used with a default
|
||||
// HTTPTransport. Using your own transport will make HTTPProxy, HTTPSProxy
|
||||
// and CaCerts options ignored.
|
||||
HTTPTransport http.RoundTripper
|
||||
// An optional HTTP proxy to use.
|
||||
// This will default to the HTTP_PROXY environment variable.
|
||||
HTTPProxy string
|
||||
// An optional HTTPS proxy to use.
|
||||
// This will default to the HTTPS_PROXY environment variable.
|
||||
// HTTPS_PROXY takes precedence over HTTP_PROXY for https requests.
|
||||
HTTPSProxy string
|
||||
// An optional set of SSL certificates to use.
|
||||
CaCerts *x509.CertPool
|
||||
// MaxErrorDepth is the maximum number of errors reported in a chain of errors.
|
||||
// This protects the SDK from an arbitrarily long chain of wrapped errors.
|
||||
//
|
||||
// An additional consideration is that arguably reporting a long chain of errors
|
||||
// is of little use when debugging production errors with Sentry. The Sentry UI
|
||||
// is not optimized for long chains either. The top-level error together with a
|
||||
// stack trace is often the most useful information.
|
||||
MaxErrorDepth int
|
||||
// Default event tags. These are overridden by tags set on a scope.
|
||||
Tags map[string]string
|
||||
// EnableLogs controls when logs should be emitted.
|
||||
EnableLogs bool
|
||||
}
|
||||
|
||||
// Client is the underlying processor that is used by the main API and Hub
|
||||
// instances. It must be created with NewClient.
|
||||
type Client struct {
|
||||
mu sync.RWMutex
|
||||
options ClientOptions
|
||||
dsn *Dsn
|
||||
eventProcessors []EventProcessor
|
||||
integrations []Integration
|
||||
sdkIdentifier string
|
||||
sdkVersion string
|
||||
// Transport is read-only. Replacing the transport of an existing client is
|
||||
// not supported, create a new client instead.
|
||||
Transport Transport
|
||||
batchLogger *BatchLogger
|
||||
}
|
||||
|
||||
// NewClient creates and returns an instance of Client configured using
|
||||
// ClientOptions.
|
||||
//
|
||||
// Most users will not create clients directly. Instead, initialize the SDK with
|
||||
// Init and use the package-level functions (for simple programs that run on a
|
||||
// single goroutine) or hub methods (for concurrent programs, for example web
|
||||
// servers).
|
||||
func NewClient(options ClientOptions) (*Client, error) {
|
||||
// The default error event sample rate for all SDKs is 1.0 (send all).
|
||||
//
|
||||
// In Go, the zero value (default) for float64 is 0.0, which means that
|
||||
// constructing a client with NewClient(ClientOptions{}), or, equivalently,
|
||||
// initializing the SDK with Init(ClientOptions{}) without an explicit
|
||||
// SampleRate would drop all events.
|
||||
//
|
||||
// To retain the desired default behavior, we exceptionally flip SampleRate
|
||||
// from 0.0 to 1.0 here. Setting the sample rate to 0.0 is not very useful
|
||||
// anyway, and the same end result can be achieved in many other ways like
|
||||
// not initializing the SDK, setting the DSN to the empty string or using an
|
||||
// event processor that always returns nil.
|
||||
//
|
||||
// An alternative API could be such that default options don't need to be
|
||||
// the same as Go's zero values, for example using the Functional Options
|
||||
// pattern. That would either require a breaking change if we want to reuse
|
||||
// the obvious NewClient name, or a new function as an alternative
|
||||
// constructor.
|
||||
if options.SampleRate == 0.0 {
|
||||
options.SampleRate = 1.0
|
||||
}
|
||||
|
||||
if options.Debug {
|
||||
debugWriter := options.DebugWriter
|
||||
if debugWriter == nil {
|
||||
debugWriter = os.Stderr
|
||||
}
|
||||
DebugLogger.SetOutput(debugWriter)
|
||||
}
|
||||
|
||||
if options.Dsn == "" {
|
||||
options.Dsn = os.Getenv("SENTRY_DSN")
|
||||
}
|
||||
|
||||
if options.Release == "" {
|
||||
options.Release = defaultRelease()
|
||||
}
|
||||
|
||||
if options.Environment == "" {
|
||||
options.Environment = os.Getenv("SENTRY_ENVIRONMENT")
|
||||
}
|
||||
|
||||
if options.MaxErrorDepth == 0 {
|
||||
options.MaxErrorDepth = maxErrorDepth
|
||||
}
|
||||
|
||||
if options.MaxSpans == 0 {
|
||||
options.MaxSpans = defaultMaxSpans
|
||||
}
|
||||
|
||||
// SENTRYGODEBUG is a comma-separated list of key=value pairs (similar
|
||||
// to GODEBUG). It is not a supported feature: recognized debug options
|
||||
// may change any time.
|
||||
//
|
||||
// The intended public is SDK developers. It is orthogonal to
|
||||
// options.Debug, which is also available for SDK users.
|
||||
dbg := strings.Split(os.Getenv("SENTRYGODEBUG"), ",")
|
||||
sort.Strings(dbg)
|
||||
// dbgOpt returns true when the given debug option is enabled, for
|
||||
// example SENTRYGODEBUG=someopt=1.
|
||||
dbgOpt := func(opt string) bool {
|
||||
s := opt + "=1"
|
||||
return dbg[sort.SearchStrings(dbg, s)%len(dbg)] == s
|
||||
}
|
||||
if dbgOpt("httpdump") || dbgOpt("httptrace") {
|
||||
options.HTTPTransport = &debug.Transport{
|
||||
RoundTripper: http.DefaultTransport,
|
||||
Output: os.Stderr,
|
||||
Dump: dbgOpt("httpdump"),
|
||||
Trace: dbgOpt("httptrace"),
|
||||
}
|
||||
}
|
||||
|
||||
var dsn *Dsn
|
||||
if options.Dsn != "" {
|
||||
var err error
|
||||
dsn, err = NewDsn(options.Dsn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
client := Client{
|
||||
options: options,
|
||||
dsn: dsn,
|
||||
sdkIdentifier: sdkIdentifier,
|
||||
sdkVersion: SDKVersion,
|
||||
}
|
||||
|
||||
if options.EnableLogs {
|
||||
client.batchLogger = NewBatchLogger(&client)
|
||||
client.batchLogger.Start()
|
||||
}
|
||||
|
||||
client.setupTransport()
|
||||
client.setupIntegrations()
|
||||
|
||||
return &client, nil
|
||||
}
|
||||
|
||||
func (client *Client) setupTransport() {
|
||||
opts := client.options
|
||||
transport := opts.Transport
|
||||
|
||||
if transport == nil {
|
||||
if opts.Dsn == "" {
|
||||
transport = new(noopTransport)
|
||||
} else {
|
||||
transport = NewHTTPTransport()
|
||||
}
|
||||
}
|
||||
|
||||
transport.Configure(opts)
|
||||
client.Transport = transport
|
||||
}
|
||||
|
||||
func (client *Client) setupIntegrations() {
|
||||
integrations := []Integration{
|
||||
new(contextifyFramesIntegration),
|
||||
new(environmentIntegration),
|
||||
new(modulesIntegration),
|
||||
new(ignoreErrorsIntegration),
|
||||
new(ignoreTransactionsIntegration),
|
||||
new(globalTagsIntegration),
|
||||
}
|
||||
|
||||
if client.options.Integrations != nil {
|
||||
integrations = client.options.Integrations(integrations)
|
||||
}
|
||||
|
||||
for _, integration := range integrations {
|
||||
if client.integrationAlreadyInstalled(integration.Name()) {
|
||||
DebugLogger.Printf("Integration %s is already installed\n", integration.Name())
|
||||
continue
|
||||
}
|
||||
client.integrations = append(client.integrations, integration)
|
||||
integration.SetupOnce(client)
|
||||
DebugLogger.Printf("Integration installed: %s\n", integration.Name())
|
||||
}
|
||||
|
||||
sort.Slice(client.integrations, func(i, j int) bool {
|
||||
return client.integrations[i].Name() < client.integrations[j].Name()
|
||||
})
|
||||
}
|
||||
|
||||
// AddEventProcessor adds an event processor to the client. It must not be
|
||||
// called from concurrent goroutines. Most users will prefer to use
|
||||
// ClientOptions.BeforeSend or Scope.AddEventProcessor instead.
|
||||
//
|
||||
// Note that typical programs have only a single client created by Init and the
|
||||
// client is shared among multiple hubs, one per goroutine, such that adding an
|
||||
// event processor to the client affects all hubs that share the client.
|
||||
func (client *Client) AddEventProcessor(processor EventProcessor) {
|
||||
client.eventProcessors = append(client.eventProcessors, processor)
|
||||
}
|
||||
|
||||
// Options return ClientOptions for the current Client.
|
||||
func (client *Client) Options() ClientOptions {
|
||||
// Note: internally, consider using `client.options` instead of `client.Options()` to avoid copying the object each time.
|
||||
return client.options
|
||||
}
|
||||
|
||||
// CaptureMessage captures an arbitrary message.
|
||||
func (client *Client) CaptureMessage(message string, hint *EventHint, scope EventModifier) *EventID {
|
||||
event := client.EventFromMessage(message, LevelInfo)
|
||||
return client.CaptureEvent(event, hint, scope)
|
||||
}
|
||||
|
||||
// CaptureException captures an error.
|
||||
func (client *Client) CaptureException(exception error, hint *EventHint, scope EventModifier) *EventID {
|
||||
event := client.EventFromException(exception, LevelError)
|
||||
return client.CaptureEvent(event, hint, scope)
|
||||
}
|
||||
|
||||
// CaptureCheckIn captures a check in.
|
||||
func (client *Client) CaptureCheckIn(checkIn *CheckIn, monitorConfig *MonitorConfig, scope EventModifier) *EventID {
|
||||
event := client.EventFromCheckIn(checkIn, monitorConfig)
|
||||
if event != nil && event.CheckIn != nil {
|
||||
client.CaptureEvent(event, nil, scope)
|
||||
return &event.CheckIn.ID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CaptureEvent captures an event on the currently active client if any.
|
||||
//
|
||||
// The event must already be assembled. Typically code would instead use
|
||||
// the utility methods like CaptureException. The return value is the
|
||||
// event ID. In case Sentry is disabled or event was dropped, the return value will be nil.
|
||||
func (client *Client) CaptureEvent(event *Event, hint *EventHint, scope EventModifier) *EventID {
|
||||
return client.processEvent(event, hint, scope)
|
||||
}
|
||||
|
||||
// Recover captures a panic.
|
||||
// Returns EventID if successfully, or nil if there's no error to recover from.
|
||||
func (client *Client) Recover(err interface{}, hint *EventHint, scope EventModifier) *EventID {
|
||||
if err == nil {
|
||||
err = recover()
|
||||
}
|
||||
|
||||
// Normally we would not pass a nil Context, but RecoverWithContext doesn't
|
||||
// use the Context for communicating deadline nor cancelation. All it does
|
||||
// is store the Context in the EventHint and there nil means the Context is
|
||||
// not available.
|
||||
// nolint: staticcheck
|
||||
return client.RecoverWithContext(nil, err, hint, scope)
|
||||
}
|
||||
|
||||
// RecoverWithContext captures a panic and passes relevant context object.
|
||||
// Returns EventID if successfully, or nil if there's no error to recover from.
|
||||
func (client *Client) RecoverWithContext(
|
||||
ctx context.Context,
|
||||
err interface{},
|
||||
hint *EventHint,
|
||||
scope EventModifier,
|
||||
) *EventID {
|
||||
if err == nil {
|
||||
err = recover()
|
||||
}
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if ctx != nil {
|
||||
if hint == nil {
|
||||
hint = &EventHint{}
|
||||
}
|
||||
if hint.Context == nil {
|
||||
hint.Context = ctx
|
||||
}
|
||||
}
|
||||
|
||||
var event *Event
|
||||
switch err := err.(type) {
|
||||
case error:
|
||||
event = client.EventFromException(err, LevelFatal)
|
||||
case string:
|
||||
event = client.EventFromMessage(err, LevelFatal)
|
||||
default:
|
||||
event = client.EventFromMessage(fmt.Sprintf("%#v", err), LevelFatal)
|
||||
}
|
||||
return client.CaptureEvent(event, hint, scope)
|
||||
}
|
||||
|
||||
// Flush waits until the underlying Transport sends any buffered events 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 CaptureEvent,
|
||||
// CaptureException or CaptureMessage. Instead, to have the SDK send events over
|
||||
// the network synchronously, configure it to use the HTTPSyncTransport in the
|
||||
// call to Init.
|
||||
func (client *Client) Flush(timeout time.Duration) bool {
|
||||
if client.batchLogger != nil {
|
||||
client.batchLogger.Flush()
|
||||
}
|
||||
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 == "" {
|
||||
err := usageError{fmt.Errorf("%s called with empty message", callerFunctionName())}
|
||||
return client.EventFromException(err, level)
|
||||
}
|
||||
event := NewEvent()
|
||||
event.Level = level
|
||||
event.Message = message
|
||||
|
||||
if client.options.AttachStacktrace {
|
||||
event.Threads = []Thread{{
|
||||
Stacktrace: NewStacktrace(),
|
||||
Crashed: false,
|
||||
Current: true,
|
||||
}}
|
||||
}
|
||||
|
||||
return event
|
||||
}
|
||||
|
||||
// EventFromException creates a new Sentry event from the given `error` instance.
|
||||
func (client *Client) EventFromException(exception error, level Level) *Event {
|
||||
event := NewEvent()
|
||||
event.Level = level
|
||||
|
||||
err := exception
|
||||
if err == nil {
|
||||
err = usageError{fmt.Errorf("%s called with nil error", callerFunctionName())}
|
||||
}
|
||||
|
||||
event.SetException(err, client.options.MaxErrorDepth)
|
||||
|
||||
return event
|
||||
}
|
||||
|
||||
// EventFromCheckIn creates a new Sentry event from the given `check_in` instance.
|
||||
func (client *Client) EventFromCheckIn(checkIn *CheckIn, monitorConfig *MonitorConfig) *Event {
|
||||
if checkIn == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
event := NewEvent()
|
||||
event.Type = checkInType
|
||||
|
||||
var checkInID EventID
|
||||
if checkIn.ID == "" {
|
||||
checkInID = EventID(uuid())
|
||||
} else {
|
||||
checkInID = checkIn.ID
|
||||
}
|
||||
|
||||
event.CheckIn = &CheckIn{
|
||||
ID: checkInID,
|
||||
MonitorSlug: checkIn.MonitorSlug,
|
||||
Status: checkIn.Status,
|
||||
Duration: checkIn.Duration,
|
||||
}
|
||||
event.MonitorConfig = monitorConfig
|
||||
|
||||
return event
|
||||
}
|
||||
|
||||
func (client *Client) SetSDKIdentifier(identifier string) {
|
||||
client.mu.Lock()
|
||||
defer client.mu.Unlock()
|
||||
|
||||
client.sdkIdentifier = identifier
|
||||
}
|
||||
|
||||
func (client *Client) GetSDKIdentifier() string {
|
||||
client.mu.RLock()
|
||||
defer client.mu.RUnlock()
|
||||
|
||||
return client.sdkIdentifier
|
||||
}
|
||||
|
||||
func (client *Client) processEvent(event *Event, hint *EventHint, scope EventModifier) *EventID {
|
||||
if event == nil {
|
||||
err := usageError{fmt.Errorf("%s called with nil event", callerFunctionName())}
|
||||
return client.CaptureException(err, hint, scope)
|
||||
}
|
||||
|
||||
// Transactions are sampled by options.TracesSampleRate or
|
||||
// options.TracesSampler when they are started. Other events
|
||||
// (errors, messages) are sampled here. Does not apply to check-ins.
|
||||
if event.Type != transactionType && event.Type != checkInType && !sample(client.options.SampleRate) {
|
||||
DebugLogger.Println("Event dropped due to SampleRate hit.")
|
||||
return nil
|
||||
}
|
||||
|
||||
if event = client.prepareEvent(event, hint, scope); event == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Apply beforeSend* processors
|
||||
if hint == nil {
|
||||
hint = &EventHint{}
|
||||
}
|
||||
switch event.Type {
|
||||
case transactionType:
|
||||
if client.options.BeforeSendTransaction != nil {
|
||||
if event = client.options.BeforeSendTransaction(event, hint); event == nil {
|
||||
DebugLogger.Println("Transaction dropped due to BeforeSendTransaction callback.")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
case checkInType: // not a default case, since we shouldn't apply BeforeSend on check-in events
|
||||
default:
|
||||
if client.options.BeforeSend != nil {
|
||||
if event = client.options.BeforeSend(event, hint); event == nil {
|
||||
DebugLogger.Println("Event dropped due to BeforeSend callback.")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client.Transport.SendEvent(event)
|
||||
|
||||
return &event.EventID
|
||||
}
|
||||
|
||||
func (client *Client) prepareEvent(event *Event, hint *EventHint, scope EventModifier) *Event {
|
||||
if event.EventID == "" {
|
||||
// TODO set EventID when the event is created, same as in other SDKs. It's necessary for profileTransaction.ID.
|
||||
event.EventID = EventID(uuid())
|
||||
}
|
||||
|
||||
if event.Timestamp.IsZero() {
|
||||
event.Timestamp = time.Now()
|
||||
}
|
||||
|
||||
if event.Level == "" {
|
||||
event.Level = LevelInfo
|
||||
}
|
||||
|
||||
if event.ServerName == "" {
|
||||
event.ServerName = client.options.ServerName
|
||||
|
||||
if event.ServerName == "" {
|
||||
event.ServerName = hostname
|
||||
}
|
||||
}
|
||||
|
||||
if event.Release == "" {
|
||||
event.Release = client.options.Release
|
||||
}
|
||||
|
||||
if event.Dist == "" {
|
||||
event.Dist = client.options.Dist
|
||||
}
|
||||
|
||||
if event.Environment == "" {
|
||||
event.Environment = client.options.Environment
|
||||
}
|
||||
|
||||
event.Platform = "go"
|
||||
event.Sdk = SdkInfo{
|
||||
Name: client.GetSDKIdentifier(),
|
||||
Version: SDKVersion,
|
||||
Integrations: client.listIntegrations(),
|
||||
Packages: []SdkPackage{{
|
||||
Name: "sentry-go",
|
||||
Version: SDKVersion,
|
||||
}},
|
||||
}
|
||||
|
||||
if scope != nil {
|
||||
event = scope.ApplyToEvent(event, hint, client)
|
||||
if event == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
for _, processor := range client.eventProcessors {
|
||||
id := event.EventID
|
||||
event = processor(event, hint)
|
||||
if event == nil {
|
||||
DebugLogger.Printf("Event dropped by one of the Client EventProcessors: %s\n", id)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
for _, processor := range globalEventProcessors {
|
||||
id := event.EventID
|
||||
event = processor(event, hint)
|
||||
if event == nil {
|
||||
DebugLogger.Printf("Event dropped by one of the Global EventProcessors: %s\n", id)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return event
|
||||
}
|
||||
|
||||
func (client *Client) listIntegrations() []string {
|
||||
integrations := make([]string, len(client.integrations))
|
||||
for i, integration := range client.integrations {
|
||||
integrations[i] = integration.Name()
|
||||
}
|
||||
return integrations
|
||||
}
|
||||
|
||||
func (client *Client) integrationAlreadyInstalled(name string) bool {
|
||||
for _, integration := range client.integrations {
|
||||
if integration.Name() == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// sample returns true with the given probability, which must be in the range
|
||||
// [0.0, 1.0].
|
||||
func sample(probability float64) bool {
|
||||
return rng.Float64() < probability
|
||||
}
|
||||
6
vendor/github.com/getsentry/sentry-go/doc.go
generated
vendored
Normal file
6
vendor/github.com/getsentry/sentry-go/doc.go
generated
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/*
|
||||
Package repository: https://github.com/getsentry/sentry-go/
|
||||
|
||||
For more information about Sentry and SDK features, please have a look at the official documentation site: https://docs.sentry.io/platforms/go/
|
||||
*/
|
||||
package sentry
|
||||
233
vendor/github.com/getsentry/sentry-go/dsn.go
generated
vendored
Normal file
233
vendor/github.com/getsentry/sentry-go/dsn.go
generated
vendored
Normal file
@@ -0,0 +1,233 @@
|
||||
package sentry
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type scheme string
|
||||
|
||||
const (
|
||||
schemeHTTP scheme = "http"
|
||||
schemeHTTPS scheme = "https"
|
||||
)
|
||||
|
||||
func (scheme scheme) defaultPort() int {
|
||||
switch scheme {
|
||||
case schemeHTTPS:
|
||||
return 443
|
||||
case schemeHTTP:
|
||||
return 80
|
||||
default:
|
||||
return 80
|
||||
}
|
||||
}
|
||||
|
||||
// DsnParseError represents an error that occurs if a Sentry
|
||||
// DSN cannot be parsed.
|
||||
type DsnParseError struct {
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e DsnParseError) Error() string {
|
||||
return "[Sentry] DsnParseError: " + e.Message
|
||||
}
|
||||
|
||||
// Dsn is used as the remote address source to client transport.
|
||||
type Dsn struct {
|
||||
scheme scheme
|
||||
publicKey string
|
||||
secretKey string
|
||||
host string
|
||||
port int
|
||||
path string
|
||||
projectID string
|
||||
}
|
||||
|
||||
// NewDsn creates a Dsn by parsing rawURL. Most users will never call this
|
||||
// function directly. It is provided for use in custom Transport
|
||||
// implementations.
|
||||
func NewDsn(rawURL string) (*Dsn, error) {
|
||||
// Parse
|
||||
parsedURL, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return nil, &DsnParseError{fmt.Sprintf("invalid url: %v", err)}
|
||||
}
|
||||
|
||||
// Scheme
|
||||
var scheme scheme
|
||||
switch parsedURL.Scheme {
|
||||
case "http":
|
||||
scheme = schemeHTTP
|
||||
case "https":
|
||||
scheme = schemeHTTPS
|
||||
default:
|
||||
return nil, &DsnParseError{"invalid scheme"}
|
||||
}
|
||||
|
||||
// PublicKey
|
||||
publicKey := parsedURL.User.Username()
|
||||
if publicKey == "" {
|
||||
return nil, &DsnParseError{"empty username"}
|
||||
}
|
||||
|
||||
// SecretKey
|
||||
var secretKey string
|
||||
if parsedSecretKey, ok := parsedURL.User.Password(); ok {
|
||||
secretKey = parsedSecretKey
|
||||
}
|
||||
|
||||
// Host
|
||||
host := parsedURL.Hostname()
|
||||
if host == "" {
|
||||
return nil, &DsnParseError{"empty host"}
|
||||
}
|
||||
|
||||
// Port
|
||||
var port int
|
||||
if p := parsedURL.Port(); p != "" {
|
||||
port, err = strconv.Atoi(p)
|
||||
if err != nil {
|
||||
return nil, &DsnParseError{"invalid port"}
|
||||
}
|
||||
} else {
|
||||
port = scheme.defaultPort()
|
||||
}
|
||||
|
||||
// ProjectID
|
||||
if parsedURL.Path == "" || parsedURL.Path == "/" {
|
||||
return nil, &DsnParseError{"empty project id"}
|
||||
}
|
||||
pathSegments := strings.Split(parsedURL.Path[1:], "/")
|
||||
projectID := pathSegments[len(pathSegments)-1]
|
||||
|
||||
if projectID == "" {
|
||||
return nil, &DsnParseError{"empty project id"}
|
||||
}
|
||||
|
||||
// Path
|
||||
var path string
|
||||
if len(pathSegments) > 1 {
|
||||
path = "/" + strings.Join(pathSegments[0:len(pathSegments)-1], "/")
|
||||
}
|
||||
|
||||
return &Dsn{
|
||||
scheme: scheme,
|
||||
publicKey: publicKey,
|
||||
secretKey: secretKey,
|
||||
host: host,
|
||||
port: port,
|
||||
path: path,
|
||||
projectID: projectID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// String formats Dsn struct into a valid string url.
|
||||
func (dsn Dsn) String() string {
|
||||
var url string
|
||||
url += fmt.Sprintf("%s://%s", dsn.scheme, dsn.publicKey)
|
||||
if dsn.secretKey != "" {
|
||||
url += fmt.Sprintf(":%s", dsn.secretKey)
|
||||
}
|
||||
url += fmt.Sprintf("@%s", dsn.host)
|
||||
if dsn.port != dsn.scheme.defaultPort() {
|
||||
url += fmt.Sprintf(":%d", dsn.port)
|
||||
}
|
||||
if dsn.path != "" {
|
||||
url += dsn.path
|
||||
}
|
||||
url += fmt.Sprintf("/%s", dsn.projectID)
|
||||
return url
|
||||
}
|
||||
|
||||
// Get the scheme of the DSN.
|
||||
func (dsn Dsn) GetScheme() string {
|
||||
return string(dsn.scheme)
|
||||
}
|
||||
|
||||
// Get the public key of the DSN.
|
||||
func (dsn Dsn) GetPublicKey() string {
|
||||
return dsn.publicKey
|
||||
}
|
||||
|
||||
// Get the secret key of the DSN.
|
||||
func (dsn Dsn) GetSecretKey() string {
|
||||
return dsn.secretKey
|
||||
}
|
||||
|
||||
// Get the host of the DSN.
|
||||
func (dsn Dsn) GetHost() string {
|
||||
return dsn.host
|
||||
}
|
||||
|
||||
// Get the port of the DSN.
|
||||
func (dsn Dsn) GetPort() int {
|
||||
return dsn.port
|
||||
}
|
||||
|
||||
// Get the path of the DSN.
|
||||
func (dsn Dsn) GetPath() string {
|
||||
return dsn.path
|
||||
}
|
||||
|
||||
// Get the project ID of the DSN.
|
||||
func (dsn Dsn) GetProjectID() string {
|
||||
return dsn.projectID
|
||||
}
|
||||
|
||||
// GetAPIURL returns the URL of the envelope endpoint of the project
|
||||
// associated with the DSN.
|
||||
func (dsn Dsn) GetAPIURL() *url.URL {
|
||||
var rawURL string
|
||||
rawURL += fmt.Sprintf("%s://%s", dsn.scheme, dsn.host)
|
||||
if dsn.port != dsn.scheme.defaultPort() {
|
||||
rawURL += fmt.Sprintf(":%d", dsn.port)
|
||||
}
|
||||
if dsn.path != "" {
|
||||
rawURL += dsn.path
|
||||
}
|
||||
rawURL += fmt.Sprintf("/api/%s/%s/", dsn.projectID, "envelope")
|
||||
parsedURL, _ := url.Parse(rawURL)
|
||||
return parsedURL
|
||||
}
|
||||
|
||||
// RequestHeaders returns all the necessary headers that have to be used in the transport when seinding events
|
||||
// to the /store endpoint.
|
||||
//
|
||||
// Deprecated: This method shall only be used if you want to implement your own transport that sends events to
|
||||
// the /store endpoint. If you're using the transport provided by the SDK, all necessary headers to authenticate
|
||||
// against the /envelope endpoint are added automatically.
|
||||
func (dsn Dsn) RequestHeaders() map[string]string {
|
||||
auth := fmt.Sprintf("Sentry sentry_version=%s, sentry_timestamp=%d, "+
|
||||
"sentry_client=sentry.go/%s, sentry_key=%s", apiVersion, time.Now().Unix(), SDKVersion, dsn.publicKey)
|
||||
|
||||
if dsn.secretKey != "" {
|
||||
auth = fmt.Sprintf("%s, sentry_secret=%s", auth, dsn.secretKey)
|
||||
}
|
||||
|
||||
return map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
"X-Sentry-Auth": auth,
|
||||
}
|
||||
}
|
||||
|
||||
// MarshalJSON converts the Dsn struct to JSON.
|
||||
func (dsn Dsn) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(dsn.String())
|
||||
}
|
||||
|
||||
// UnmarshalJSON converts JSON data to the Dsn struct.
|
||||
func (dsn *Dsn) UnmarshalJSON(data []byte) error {
|
||||
var str string
|
||||
_ = json.Unmarshal(data, &str)
|
||||
newDsn, err := NewDsn(str)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*dsn = *newDsn
|
||||
return nil
|
||||
}
|
||||
154
vendor/github.com/getsentry/sentry-go/dynamic_sampling_context.go
generated
vendored
Normal file
154
vendor/github.com/getsentry/sentry-go/dynamic_sampling_context.go
generated
vendored
Normal file
@@ -0,0 +1,154 @@
|
||||
package sentry
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/getsentry/sentry-go/internal/otel/baggage"
|
||||
)
|
||||
|
||||
const (
|
||||
sentryPrefix = "sentry-"
|
||||
)
|
||||
|
||||
// DynamicSamplingContext holds information about the current event that can be used to make dynamic sampling decisions.
|
||||
type DynamicSamplingContext struct {
|
||||
Entries map[string]string
|
||||
Frozen bool
|
||||
}
|
||||
|
||||
func DynamicSamplingContextFromHeader(header []byte) (DynamicSamplingContext, error) {
|
||||
bag, err := baggage.Parse(string(header))
|
||||
if err != nil {
|
||||
return DynamicSamplingContext{}, err
|
||||
}
|
||||
|
||||
entries := map[string]string{}
|
||||
for _, member := range bag.Members() {
|
||||
// We only store baggage members if their key starts with "sentry-".
|
||||
if k, v := member.Key(), member.Value(); strings.HasPrefix(k, sentryPrefix) {
|
||||
entries[strings.TrimPrefix(k, sentryPrefix)] = v
|
||||
}
|
||||
}
|
||||
|
||||
return DynamicSamplingContext{
|
||||
Entries: entries,
|
||||
// If there's at least one Sentry value, we consider the DSC frozen
|
||||
Frozen: len(entries) > 0,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func DynamicSamplingContextFromTransaction(span *Span) DynamicSamplingContext {
|
||||
hub := hubFromContext(span.Context())
|
||||
scope := hub.Scope()
|
||||
client := hub.Client()
|
||||
|
||||
if client == nil || scope == nil {
|
||||
return DynamicSamplingContext{
|
||||
Entries: map[string]string{},
|
||||
Frozen: false,
|
||||
}
|
||||
}
|
||||
|
||||
entries := make(map[string]string)
|
||||
|
||||
if traceID := span.TraceID.String(); traceID != "" {
|
||||
entries["trace_id"] = traceID
|
||||
}
|
||||
if sampleRate := span.sampleRate; 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
|
||||
}
|
||||
|
||||
// Only include the transaction name if it's of good quality (not empty and not SourceURL)
|
||||
if span.Source != "" && span.Source != SourceURL {
|
||||
if span.IsTransaction() {
|
||||
entries["transaction"] = span.Name
|
||||
}
|
||||
}
|
||||
|
||||
entries["sampled"] = strconv.FormatBool(span.Sampled.Bool())
|
||||
|
||||
return DynamicSamplingContext{Entries: entries, Frozen: true}
|
||||
}
|
||||
|
||||
func (d DynamicSamplingContext) HasEntries() bool {
|
||||
return len(d.Entries) > 0
|
||||
}
|
||||
|
||||
func (d DynamicSamplingContext) IsFrozen() bool {
|
||||
return d.Frozen
|
||||
}
|
||||
|
||||
func (d DynamicSamplingContext) String() string {
|
||||
members := []baggage.Member{}
|
||||
for k, entry := range d.Entries {
|
||||
member, err := baggage.NewMember(sentryPrefix+k, entry)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
members = append(members, member)
|
||||
}
|
||||
|
||||
if len(members) == 0 {
|
||||
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,
|
||||
}
|
||||
}
|
||||
423
vendor/github.com/getsentry/sentry-go/hub.go
generated
vendored
Normal file
423
vendor/github.com/getsentry/sentry-go/hub.go
generated
vendored
Normal file
@@ -0,0 +1,423 @@
|
||||
package sentry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type contextKey int
|
||||
|
||||
// Keys used to store values in a Context. Use with Context.Value to access
|
||||
// values stored by the SDK.
|
||||
const (
|
||||
// HubContextKey is the key used to store the current Hub.
|
||||
HubContextKey = contextKey(1)
|
||||
// RequestContextKey is the key used to store the current http.Request.
|
||||
RequestContextKey = contextKey(2)
|
||||
)
|
||||
|
||||
// defaultMaxBreadcrumbs is the default maximum number of breadcrumbs added to
|
||||
// an event. Can be overwritten with the maxBreadcrumbs option.
|
||||
const defaultMaxBreadcrumbs = 30
|
||||
|
||||
// maxBreadcrumbs is the absolute maximum number of breadcrumbs added to an
|
||||
// event. The maxBreadcrumbs option cannot be set higher than this value.
|
||||
const maxBreadcrumbs = 100
|
||||
|
||||
// currentHub is the initial Hub with no Client bound and an empty Scope.
|
||||
var currentHub = NewHub(nil, NewScope())
|
||||
|
||||
// Hub is the central object that manages scopes and clients.
|
||||
//
|
||||
// This can be used to capture events and manage the scope.
|
||||
// The default hub that is available automatically.
|
||||
//
|
||||
// In most situations developers do not need to interface the hub. Instead
|
||||
// toplevel convenience functions are exposed that will automatically dispatch
|
||||
// to global (CurrentHub) hub. In some situations this might not be
|
||||
// possible in which case it might become necessary to manually work with the
|
||||
// hub. This is for instance the case when working with async code.
|
||||
type Hub struct {
|
||||
mu sync.RWMutex
|
||||
stack *stack
|
||||
lastEventID EventID
|
||||
}
|
||||
|
||||
type layer struct {
|
||||
// mu protects concurrent reads and writes to client.
|
||||
mu sync.RWMutex
|
||||
client *Client
|
||||
// scope is read-only, not protected by mu.
|
||||
scope *Scope
|
||||
}
|
||||
|
||||
// Client returns the layer's client. Safe for concurrent use.
|
||||
func (l *layer) Client() *Client {
|
||||
l.mu.RLock()
|
||||
defer l.mu.RUnlock()
|
||||
return l.client
|
||||
}
|
||||
|
||||
// SetClient sets the layer's client. Safe for concurrent use.
|
||||
func (l *layer) SetClient(c *Client) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
l.client = c
|
||||
}
|
||||
|
||||
type stack []*layer
|
||||
|
||||
// NewHub returns an instance of a Hub with provided Client and Scope bound.
|
||||
func NewHub(client *Client, scope *Scope) *Hub {
|
||||
hub := Hub{
|
||||
stack: &stack{{
|
||||
client: client,
|
||||
scope: scope,
|
||||
}},
|
||||
}
|
||||
return &hub
|
||||
}
|
||||
|
||||
// CurrentHub returns an instance of previously initialized Hub stored in the global namespace.
|
||||
func CurrentHub() *Hub {
|
||||
return currentHub
|
||||
}
|
||||
|
||||
// LastEventID returns the ID of the last event (error or message) captured
|
||||
// through the hub and sent to the underlying transport.
|
||||
//
|
||||
// Transactions and events dropped by sampling or event processors do not change
|
||||
// the last event ID.
|
||||
//
|
||||
// LastEventID is a convenience method to cover use cases in which errors are
|
||||
// captured indirectly and the ID is needed. For example, it can be used as part
|
||||
// of an HTTP middleware to log the ID of the last error, if any.
|
||||
//
|
||||
// For more flexibility, consider instead using the ClientOptions.BeforeSend
|
||||
// function or event processors.
|
||||
func (hub *Hub) LastEventID() EventID {
|
||||
hub.mu.RLock()
|
||||
defer hub.mu.RUnlock()
|
||||
|
||||
return hub.lastEventID
|
||||
}
|
||||
|
||||
// stackTop returns the top layer of the hub stack. Valid hubs always have at
|
||||
// least one layer, therefore stackTop always return a non-nil pointer.
|
||||
func (hub *Hub) stackTop() *layer {
|
||||
hub.mu.RLock()
|
||||
defer hub.mu.RUnlock()
|
||||
|
||||
stack := hub.stack
|
||||
stackLen := len(*stack)
|
||||
top := (*stack)[stackLen-1]
|
||||
return top
|
||||
}
|
||||
|
||||
// Clone returns a copy of the current Hub with top-most scope and client copied over.
|
||||
func (hub *Hub) Clone() *Hub {
|
||||
top := hub.stackTop()
|
||||
scope := top.scope
|
||||
if scope != nil {
|
||||
scope = scope.Clone()
|
||||
}
|
||||
return NewHub(top.Client(), scope)
|
||||
}
|
||||
|
||||
// Scope returns top-level Scope of the current Hub or nil if no Scope is bound.
|
||||
func (hub *Hub) Scope() *Scope {
|
||||
top := hub.stackTop()
|
||||
return top.scope
|
||||
}
|
||||
|
||||
// Client returns top-level Client of the current Hub or nil if no Client is bound.
|
||||
func (hub *Hub) Client() *Client {
|
||||
top := hub.stackTop()
|
||||
return top.Client()
|
||||
}
|
||||
|
||||
// PushScope pushes a new scope for the current Hub and reuses previously bound Client.
|
||||
func (hub *Hub) PushScope() *Scope {
|
||||
top := hub.stackTop()
|
||||
|
||||
var scope *Scope
|
||||
if top.scope != nil {
|
||||
scope = top.scope.Clone()
|
||||
} else {
|
||||
scope = NewScope()
|
||||
}
|
||||
|
||||
hub.mu.Lock()
|
||||
defer hub.mu.Unlock()
|
||||
|
||||
*hub.stack = append(*hub.stack, &layer{
|
||||
client: top.Client(),
|
||||
scope: scope,
|
||||
})
|
||||
|
||||
return scope
|
||||
}
|
||||
|
||||
// PopScope drops the most recent scope.
|
||||
//
|
||||
// Calls to PopScope must be coordinated with PushScope. For most cases, using
|
||||
// WithScope should be more convenient.
|
||||
//
|
||||
// Calls to PopScope that do not match previous calls to PushScope are silently
|
||||
// ignored.
|
||||
func (hub *Hub) PopScope() {
|
||||
hub.mu.Lock()
|
||||
defer hub.mu.Unlock()
|
||||
|
||||
stack := *hub.stack
|
||||
stackLen := len(stack)
|
||||
if stackLen > 1 {
|
||||
// Never pop the last item off the stack, the stack should always have
|
||||
// at least one item.
|
||||
*hub.stack = stack[0 : stackLen-1]
|
||||
}
|
||||
}
|
||||
|
||||
// BindClient binds a new Client for the current Hub.
|
||||
func (hub *Hub) BindClient(client *Client) {
|
||||
top := hub.stackTop()
|
||||
top.SetClient(client)
|
||||
}
|
||||
|
||||
// WithScope runs f in an isolated temporary scope.
|
||||
//
|
||||
// It is useful when extra data should be sent with a single capture call, for
|
||||
// instance a different level or tags.
|
||||
//
|
||||
// The scope passed to f starts as a clone of the current scope and can be
|
||||
// freely modified without affecting the current scope.
|
||||
//
|
||||
// It is a shorthand for PushScope followed by PopScope.
|
||||
func (hub *Hub) WithScope(f func(scope *Scope)) {
|
||||
scope := hub.PushScope()
|
||||
defer hub.PopScope()
|
||||
f(scope)
|
||||
}
|
||||
|
||||
// ConfigureScope runs f in the current scope.
|
||||
//
|
||||
// It is useful to set data that applies to all events that share the current
|
||||
// scope.
|
||||
//
|
||||
// Modifying the scope affects all references to the current scope.
|
||||
//
|
||||
// See also WithScope for making isolated temporary changes.
|
||||
func (hub *Hub) ConfigureScope(f func(scope *Scope)) {
|
||||
scope := hub.Scope()
|
||||
f(scope)
|
||||
}
|
||||
|
||||
// CaptureEvent calls the method of a same name on currently bound Client instance
|
||||
// passing it a top-level Scope.
|
||||
// Returns EventID if successfully, or nil if there's no Scope or Client available.
|
||||
func (hub *Hub) CaptureEvent(event *Event) *EventID {
|
||||
client, scope := hub.Client(), hub.Scope()
|
||||
if client == nil || scope == nil {
|
||||
return nil
|
||||
}
|
||||
eventID := client.CaptureEvent(event, nil, scope)
|
||||
|
||||
if event.Type != transactionType && eventID != nil {
|
||||
hub.mu.Lock()
|
||||
hub.lastEventID = *eventID
|
||||
hub.mu.Unlock()
|
||||
}
|
||||
return eventID
|
||||
}
|
||||
|
||||
// CaptureMessage calls the method of a same name on currently bound Client instance
|
||||
// passing it a top-level Scope.
|
||||
// Returns EventID if successfully, or nil if there's no Scope or Client available.
|
||||
func (hub *Hub) CaptureMessage(message string) *EventID {
|
||||
client, scope := hub.Client(), hub.Scope()
|
||||
if client == nil || scope == nil {
|
||||
return nil
|
||||
}
|
||||
eventID := client.CaptureMessage(message, nil, scope)
|
||||
|
||||
if eventID != nil {
|
||||
hub.mu.Lock()
|
||||
hub.lastEventID = *eventID
|
||||
hub.mu.Unlock()
|
||||
}
|
||||
return eventID
|
||||
}
|
||||
|
||||
// CaptureException calls the method of a same name on currently bound Client instance
|
||||
// passing it a top-level Scope.
|
||||
// Returns EventID if successfully, or nil if there's no Scope or Client available.
|
||||
func (hub *Hub) CaptureException(exception error) *EventID {
|
||||
client, scope := hub.Client(), hub.Scope()
|
||||
if client == nil || scope == nil {
|
||||
return nil
|
||||
}
|
||||
eventID := client.CaptureException(exception, &EventHint{OriginalException: exception}, scope)
|
||||
|
||||
if eventID != nil {
|
||||
hub.mu.Lock()
|
||||
hub.lastEventID = *eventID
|
||||
hub.mu.Unlock()
|
||||
}
|
||||
return eventID
|
||||
}
|
||||
|
||||
// CaptureCheckIn calls the method of the same name on currently bound Client instance
|
||||
// passing it a top-level Scope.
|
||||
// Returns CheckInID if the check-in was captured successfully, or nil otherwise.
|
||||
func (hub *Hub) CaptureCheckIn(checkIn *CheckIn, monitorConfig *MonitorConfig) *EventID {
|
||||
client, scope := hub.Client(), hub.Scope()
|
||||
if client == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return client.CaptureCheckIn(checkIn, monitorConfig, scope)
|
||||
}
|
||||
|
||||
// AddBreadcrumb records a new breadcrumb.
|
||||
//
|
||||
// The total number of breadcrumbs that can be recorded are limited by the
|
||||
// configuration on the client.
|
||||
func (hub *Hub) AddBreadcrumb(breadcrumb *Breadcrumb, hint *BreadcrumbHint) {
|
||||
client := hub.Client()
|
||||
|
||||
// If there's no client, just store it on the scope straight away
|
||||
if client == nil {
|
||||
hub.Scope().AddBreadcrumb(breadcrumb, maxBreadcrumbs)
|
||||
return
|
||||
}
|
||||
|
||||
limit := client.options.MaxBreadcrumbs
|
||||
switch {
|
||||
case limit < 0:
|
||||
return
|
||||
case limit == 0:
|
||||
limit = defaultMaxBreadcrumbs
|
||||
case limit > maxBreadcrumbs:
|
||||
limit = maxBreadcrumbs
|
||||
}
|
||||
|
||||
if client.options.BeforeBreadcrumb != nil {
|
||||
if hint == nil {
|
||||
hint = &BreadcrumbHint{}
|
||||
}
|
||||
if breadcrumb = client.options.BeforeBreadcrumb(breadcrumb, hint); breadcrumb == nil {
|
||||
DebugLogger.Println("breadcrumb dropped due to BeforeBreadcrumb callback.")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
hub.Scope().AddBreadcrumb(breadcrumb, limit)
|
||||
}
|
||||
|
||||
// Recover calls the method of a same name on currently bound Client instance
|
||||
// passing it a top-level Scope.
|
||||
// Returns EventID if successfully, or nil if there's no Scope or Client available.
|
||||
func (hub *Hub) Recover(err interface{}) *EventID {
|
||||
if err == nil {
|
||||
err = recover()
|
||||
}
|
||||
client, scope := hub.Client(), hub.Scope()
|
||||
if client == nil || scope == nil {
|
||||
return nil
|
||||
}
|
||||
return client.Recover(err, &EventHint{RecoveredException: err}, scope)
|
||||
}
|
||||
|
||||
// RecoverWithContext calls the method of a same name on currently bound Client instance
|
||||
// passing it a top-level Scope.
|
||||
// Returns EventID if successfully, or nil if there's no Scope or Client available.
|
||||
func (hub *Hub) RecoverWithContext(ctx context.Context, err interface{}) *EventID {
|
||||
if err == nil {
|
||||
err = recover()
|
||||
}
|
||||
client, scope := hub.Client(), hub.Scope()
|
||||
if client == nil || scope == nil {
|
||||
return nil
|
||||
}
|
||||
return client.RecoverWithContext(ctx, err, &EventHint{RecoveredException: err}, scope)
|
||||
}
|
||||
|
||||
// Flush waits until the underlying Transport sends any buffered events 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 CaptureEvent,
|
||||
// CaptureException or CaptureMessage. Instead, to have the SDK send events over
|
||||
// the network synchronously, configure it to use the HTTPSyncTransport in the
|
||||
// call to Init.
|
||||
func (hub *Hub) Flush(timeout time.Duration) bool {
|
||||
client := hub.Client()
|
||||
|
||||
if client == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
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)
|
||||
return ok
|
||||
}
|
||||
|
||||
// GetHubFromContext tries to retrieve Hub instance from the given Context struct
|
||||
// or return nil if one is not found.
|
||||
func GetHubFromContext(ctx context.Context) *Hub {
|
||||
if hub, ok := ctx.Value(HubContextKey).(*Hub); ok {
|
||||
return hub
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// hubFromContext returns either a hub stored in the context or the current hub.
|
||||
// The return value is guaranteed to be non-nil, unlike GetHubFromContext.
|
||||
func hubFromContext(ctx context.Context) *Hub {
|
||||
if hub, ok := ctx.Value(HubContextKey).(*Hub); ok {
|
||||
return hub
|
||||
}
|
||||
return currentHub
|
||||
}
|
||||
|
||||
// SetHubOnContext stores given Hub instance on the Context struct and returns a new Context.
|
||||
func SetHubOnContext(ctx context.Context, hub *Hub) context.Context {
|
||||
return context.WithValue(ctx, HubContextKey, hub)
|
||||
}
|
||||
391
vendor/github.com/getsentry/sentry-go/integrations.go
generated
vendored
Normal file
391
vendor/github.com/getsentry/sentry-go/integrations.go
generated
vendored
Normal file
@@ -0,0 +1,391 @@
|
||||
package sentry
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// ================================
|
||||
// Modules Integration
|
||||
// ================================
|
||||
|
||||
type modulesIntegration struct {
|
||||
once sync.Once
|
||||
modules map[string]string
|
||||
}
|
||||
|
||||
func (mi *modulesIntegration) Name() string {
|
||||
return "Modules"
|
||||
}
|
||||
|
||||
func (mi *modulesIntegration) SetupOnce(client *Client) {
|
||||
client.AddEventProcessor(mi.processor)
|
||||
}
|
||||
|
||||
func (mi *modulesIntegration) processor(event *Event, _ *EventHint) *Event {
|
||||
if len(event.Modules) == 0 {
|
||||
mi.once.Do(func() {
|
||||
info, ok := debug.ReadBuildInfo()
|
||||
if !ok {
|
||||
DebugLogger.Print("The Modules integration is not available in binaries built without module support.")
|
||||
return
|
||||
}
|
||||
mi.modules = extractModules(info)
|
||||
})
|
||||
}
|
||||
event.Modules = mi.modules
|
||||
return event
|
||||
}
|
||||
|
||||
func extractModules(info *debug.BuildInfo) map[string]string {
|
||||
modules := map[string]string{
|
||||
info.Main.Path: info.Main.Version,
|
||||
}
|
||||
for _, dep := range info.Deps {
|
||||
ver := dep.Version
|
||||
if dep.Replace != nil {
|
||||
ver += fmt.Sprintf(" => %s %s", dep.Replace.Path, dep.Replace.Version)
|
||||
}
|
||||
modules[dep.Path] = strings.TrimSuffix(ver, " ")
|
||||
}
|
||||
return modules
|
||||
}
|
||||
|
||||
// ================================
|
||||
// Environment Integration
|
||||
// ================================
|
||||
|
||||
type environmentIntegration struct{}
|
||||
|
||||
func (ei *environmentIntegration) Name() string {
|
||||
return "Environment"
|
||||
}
|
||||
|
||||
func (ei *environmentIntegration) SetupOnce(client *Client) {
|
||||
client.AddEventProcessor(ei.processor)
|
||||
}
|
||||
|
||||
func (ei *environmentIntegration) processor(event *Event, _ *EventHint) *Event {
|
||||
// Initialize maps as necessary.
|
||||
contextNames := []string{"device", "os", "runtime"}
|
||||
if event.Contexts == nil {
|
||||
event.Contexts = make(map[string]Context, len(contextNames))
|
||||
}
|
||||
for _, name := range contextNames {
|
||||
if event.Contexts[name] == nil {
|
||||
event.Contexts[name] = make(Context)
|
||||
}
|
||||
}
|
||||
|
||||
// Set contextual information preserving existing data. For each context, if
|
||||
// the existing value is not of type map[string]interface{}, then no
|
||||
// additional information is added.
|
||||
if deviceContext, ok := event.Contexts["device"]; ok {
|
||||
if _, ok := deviceContext["arch"]; !ok {
|
||||
deviceContext["arch"] = runtime.GOARCH
|
||||
}
|
||||
if _, ok := deviceContext["num_cpu"]; !ok {
|
||||
deviceContext["num_cpu"] = runtime.NumCPU()
|
||||
}
|
||||
}
|
||||
if osContext, ok := event.Contexts["os"]; ok {
|
||||
if _, ok := osContext["name"]; !ok {
|
||||
osContext["name"] = runtime.GOOS
|
||||
}
|
||||
}
|
||||
if runtimeContext, ok := event.Contexts["runtime"]; ok {
|
||||
if _, ok := runtimeContext["name"]; !ok {
|
||||
runtimeContext["name"] = "go"
|
||||
}
|
||||
if _, ok := runtimeContext["version"]; !ok {
|
||||
runtimeContext["version"] = runtime.Version()
|
||||
}
|
||||
if _, ok := runtimeContext["go_numroutines"]; !ok {
|
||||
runtimeContext["go_numroutines"] = runtime.NumGoroutine()
|
||||
}
|
||||
if _, ok := runtimeContext["go_maxprocs"]; !ok {
|
||||
runtimeContext["go_maxprocs"] = runtime.GOMAXPROCS(0)
|
||||
}
|
||||
if _, ok := runtimeContext["go_numcgocalls"]; !ok {
|
||||
runtimeContext["go_numcgocalls"] = runtime.NumCgoCall()
|
||||
}
|
||||
}
|
||||
return event
|
||||
}
|
||||
|
||||
// ================================
|
||||
// Ignore Errors Integration
|
||||
// ================================
|
||||
|
||||
type ignoreErrorsIntegration struct {
|
||||
ignoreErrors []*regexp.Regexp
|
||||
}
|
||||
|
||||
func (iei *ignoreErrorsIntegration) Name() string {
|
||||
return "IgnoreErrors"
|
||||
}
|
||||
|
||||
func (iei *ignoreErrorsIntegration) SetupOnce(client *Client) {
|
||||
iei.ignoreErrors = transformStringsIntoRegexps(client.options.IgnoreErrors)
|
||||
client.AddEventProcessor(iei.processor)
|
||||
}
|
||||
|
||||
func (iei *ignoreErrorsIntegration) processor(event *Event, _ *EventHint) *Event {
|
||||
suspects := getIgnoreErrorsSuspects(event)
|
||||
|
||||
for _, suspect := range suspects {
|
||||
for _, pattern := range iei.ignoreErrors {
|
||||
if pattern.Match([]byte(suspect)) || strings.Contains(suspect, pattern.String()) {
|
||||
DebugLogger.Printf("Event dropped due to being matched by `IgnoreErrors` option."+
|
||||
"| Value matched: %s | Filter used: %s", suspect, pattern)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return event
|
||||
}
|
||||
|
||||
func transformStringsIntoRegexps(strings []string) []*regexp.Regexp {
|
||||
var exprs []*regexp.Regexp
|
||||
|
||||
for _, s := range strings {
|
||||
r, err := regexp.Compile(s)
|
||||
if err == nil {
|
||||
exprs = append(exprs, r)
|
||||
}
|
||||
}
|
||||
|
||||
return exprs
|
||||
}
|
||||
|
||||
func getIgnoreErrorsSuspects(event *Event) []string {
|
||||
suspects := []string{}
|
||||
|
||||
if event.Message != "" {
|
||||
suspects = append(suspects, event.Message)
|
||||
}
|
||||
|
||||
for _, ex := range event.Exception {
|
||||
suspects = append(suspects, ex.Type, ex.Value)
|
||||
}
|
||||
|
||||
return suspects
|
||||
}
|
||||
|
||||
// ================================
|
||||
// Ignore Transactions Integration
|
||||
// ================================
|
||||
|
||||
type ignoreTransactionsIntegration struct {
|
||||
ignoreTransactions []*regexp.Regexp
|
||||
}
|
||||
|
||||
func (iei *ignoreTransactionsIntegration) Name() string {
|
||||
return "IgnoreTransactions"
|
||||
}
|
||||
|
||||
func (iei *ignoreTransactionsIntegration) SetupOnce(client *Client) {
|
||||
iei.ignoreTransactions = transformStringsIntoRegexps(client.options.IgnoreTransactions)
|
||||
client.AddEventProcessor(iei.processor)
|
||||
}
|
||||
|
||||
func (iei *ignoreTransactionsIntegration) processor(event *Event, _ *EventHint) *Event {
|
||||
suspect := event.Transaction
|
||||
if suspect == "" {
|
||||
return event
|
||||
}
|
||||
|
||||
for _, pattern := range iei.ignoreTransactions {
|
||||
if pattern.Match([]byte(suspect)) || strings.Contains(suspect, pattern.String()) {
|
||||
DebugLogger.Printf("Transaction dropped due to being matched by `IgnoreTransactions` option."+
|
||||
"| Value matched: %s | Filter used: %s", suspect, pattern)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return event
|
||||
}
|
||||
|
||||
// ================================
|
||||
// Contextify Frames Integration
|
||||
// ================================
|
||||
|
||||
type contextifyFramesIntegration struct {
|
||||
sr sourceReader
|
||||
contextLines int
|
||||
cachedLocations sync.Map
|
||||
}
|
||||
|
||||
func (cfi *contextifyFramesIntegration) Name() string {
|
||||
return "ContextifyFrames"
|
||||
}
|
||||
|
||||
func (cfi *contextifyFramesIntegration) SetupOnce(client *Client) {
|
||||
cfi.sr = newSourceReader()
|
||||
cfi.contextLines = 5
|
||||
|
||||
client.AddEventProcessor(cfi.processor)
|
||||
}
|
||||
|
||||
func (cfi *contextifyFramesIntegration) processor(event *Event, _ *EventHint) *Event {
|
||||
// Range over all exceptions
|
||||
for _, ex := range event.Exception {
|
||||
// If it has no stacktrace, just bail out
|
||||
if ex.Stacktrace == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// If it does, it should have frames, so try to contextify them
|
||||
ex.Stacktrace.Frames = cfi.contextify(ex.Stacktrace.Frames)
|
||||
}
|
||||
|
||||
// Range over all threads
|
||||
for _, th := range event.Threads {
|
||||
// If it has no stacktrace, just bail out
|
||||
if th.Stacktrace == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// If it does, it should have frames, so try to contextify them
|
||||
th.Stacktrace.Frames = cfi.contextify(th.Stacktrace.Frames)
|
||||
}
|
||||
|
||||
return event
|
||||
}
|
||||
|
||||
func (cfi *contextifyFramesIntegration) contextify(frames []Frame) []Frame {
|
||||
contextifiedFrames := make([]Frame, 0, len(frames))
|
||||
|
||||
for _, frame := range frames {
|
||||
if !frame.InApp {
|
||||
contextifiedFrames = append(contextifiedFrames, frame)
|
||||
continue
|
||||
}
|
||||
|
||||
var path string
|
||||
|
||||
if cachedPath, ok := cfi.cachedLocations.Load(frame.AbsPath); ok {
|
||||
if p, ok := cachedPath.(string); ok {
|
||||
path = p
|
||||
}
|
||||
} else {
|
||||
// Optimize for happy path here
|
||||
if fileExists(frame.AbsPath) {
|
||||
path = frame.AbsPath
|
||||
} else {
|
||||
path = cfi.findNearbySourceCodeLocation(frame.AbsPath)
|
||||
}
|
||||
}
|
||||
|
||||
if path == "" {
|
||||
contextifiedFrames = append(contextifiedFrames, frame)
|
||||
continue
|
||||
}
|
||||
|
||||
lines, contextLine := cfi.sr.readContextLines(path, frame.Lineno, cfi.contextLines)
|
||||
contextifiedFrames = append(contextifiedFrames, cfi.addContextLinesToFrame(frame, lines, contextLine))
|
||||
}
|
||||
|
||||
return contextifiedFrames
|
||||
}
|
||||
|
||||
func (cfi *contextifyFramesIntegration) findNearbySourceCodeLocation(originalPath string) string {
|
||||
trimmedPath := strings.TrimPrefix(originalPath, "/")
|
||||
components := strings.Split(trimmedPath, "/")
|
||||
|
||||
for len(components) > 0 {
|
||||
components = components[1:]
|
||||
possibleLocation := strings.Join(components, "/")
|
||||
|
||||
if fileExists(possibleLocation) {
|
||||
cfi.cachedLocations.Store(originalPath, possibleLocation)
|
||||
return possibleLocation
|
||||
}
|
||||
}
|
||||
|
||||
cfi.cachedLocations.Store(originalPath, "")
|
||||
return ""
|
||||
}
|
||||
|
||||
func (cfi *contextifyFramesIntegration) addContextLinesToFrame(frame Frame, lines [][]byte, contextLine int) Frame {
|
||||
for i, line := range lines {
|
||||
switch {
|
||||
case i < contextLine:
|
||||
frame.PreContext = append(frame.PreContext, string(line))
|
||||
case i == contextLine:
|
||||
frame.ContextLine = string(line)
|
||||
default:
|
||||
frame.PostContext = append(frame.PostContext, string(line))
|
||||
}
|
||||
}
|
||||
return frame
|
||||
}
|
||||
|
||||
// ================================
|
||||
// Global Tags Integration
|
||||
// ================================
|
||||
|
||||
const envTagsPrefix = "SENTRY_TAGS_"
|
||||
|
||||
type globalTagsIntegration struct {
|
||||
tags map[string]string
|
||||
envTags map[string]string
|
||||
}
|
||||
|
||||
func (ti *globalTagsIntegration) Name() string {
|
||||
return "GlobalTags"
|
||||
}
|
||||
|
||||
func (ti *globalTagsIntegration) SetupOnce(client *Client) {
|
||||
ti.tags = make(map[string]string, len(client.options.Tags))
|
||||
for k, v := range client.options.Tags {
|
||||
ti.tags[k] = v
|
||||
}
|
||||
|
||||
ti.envTags = loadEnvTags()
|
||||
|
||||
client.AddEventProcessor(ti.processor)
|
||||
}
|
||||
|
||||
func (ti *globalTagsIntegration) processor(event *Event, _ *EventHint) *Event {
|
||||
if len(ti.tags) == 0 && len(ti.envTags) == 0 {
|
||||
return event
|
||||
}
|
||||
|
||||
if event.Tags == nil {
|
||||
event.Tags = make(map[string]string, len(ti.tags)+len(ti.envTags))
|
||||
}
|
||||
|
||||
for k, v := range ti.tags {
|
||||
if _, ok := event.Tags[k]; !ok {
|
||||
event.Tags[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
for k, v := range ti.envTags {
|
||||
if _, ok := event.Tags[k]; !ok {
|
||||
event.Tags[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return event
|
||||
}
|
||||
|
||||
func loadEnvTags() map[string]string {
|
||||
tags := map[string]string{}
|
||||
for _, pair := range os.Environ() {
|
||||
parts := strings.Split(pair, "=")
|
||||
if !strings.HasPrefix(parts[0], envTagsPrefix) {
|
||||
continue
|
||||
}
|
||||
tag := strings.TrimPrefix(parts[0], envTagsPrefix)
|
||||
tags[tag] = parts[1]
|
||||
}
|
||||
return tags
|
||||
}
|
||||
625
vendor/github.com/getsentry/sentry-go/interfaces.go
generated
vendored
Normal file
625
vendor/github.com/getsentry/sentry-go/interfaces.go
generated
vendored
Normal file
@@ -0,0 +1,625 @@
|
||||
package sentry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/getsentry/sentry-go/attribute"
|
||||
)
|
||||
|
||||
const eventType = "event"
|
||||
const transactionType = "transaction"
|
||||
const checkInType = "check_in"
|
||||
|
||||
var logEvent = struct {
|
||||
Type string
|
||||
ContentType string
|
||||
}{
|
||||
"log",
|
||||
"application/vnd.sentry.items.log+json",
|
||||
}
|
||||
|
||||
// Level marks the severity of the event.
|
||||
type Level string
|
||||
|
||||
// Describes the severity of the event.
|
||||
const (
|
||||
LevelDebug Level = "debug"
|
||||
LevelInfo Level = "info"
|
||||
LevelWarning Level = "warning"
|
||||
LevelError Level = "error"
|
||||
LevelFatal Level = "fatal"
|
||||
)
|
||||
|
||||
// SdkInfo contains all metadata about the SDK.
|
||||
type SdkInfo struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Integrations []string `json:"integrations,omitempty"`
|
||||
Packages []SdkPackage `json:"packages,omitempty"`
|
||||
}
|
||||
|
||||
// SdkPackage describes a package that was installed.
|
||||
type SdkPackage struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
}
|
||||
|
||||
// TODO: This type could be more useful, as map of interface{} is too generic
|
||||
// and requires a lot of type assertions in beforeBreadcrumb calls
|
||||
// plus it could just be map[string]interface{} then.
|
||||
|
||||
// BreadcrumbHint contains information that can be associated with a Breadcrumb.
|
||||
type BreadcrumbHint map[string]interface{}
|
||||
|
||||
// Breadcrumb specifies an application event that occurred before a Sentry event.
|
||||
// An event may contain one or more breadcrumbs.
|
||||
type Breadcrumb struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
Category string `json:"category,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Data map[string]interface{} `json:"data,omitempty"`
|
||||
Level Level `json:"level,omitempty"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
}
|
||||
|
||||
// TODO: provide constants for known breadcrumb types.
|
||||
// See https://develop.sentry.dev/sdk/event-payloads/breadcrumbs/#breadcrumb-types.
|
||||
|
||||
// MarshalJSON converts the Breadcrumb struct to JSON.
|
||||
func (b *Breadcrumb) MarshalJSON() ([]byte, error) {
|
||||
// We want to omit time.Time zero values, otherwise the server will try to
|
||||
// interpret dates too far in the past. However, encoding/json doesn't
|
||||
// support the "omitempty" option for struct types. See
|
||||
// https://golang.org/issues/11939.
|
||||
//
|
||||
// We overcome the limitation and achieve what we want by shadowing fields
|
||||
// and a few type tricks.
|
||||
|
||||
// breadcrumb aliases Breadcrumb to allow calling json.Marshal without an
|
||||
// infinite loop. It preserves all fields while none of the attached
|
||||
// methods.
|
||||
type breadcrumb Breadcrumb
|
||||
|
||||
if b.Timestamp.IsZero() {
|
||||
return json.Marshal(struct {
|
||||
// Embed all of the fields of Breadcrumb.
|
||||
*breadcrumb
|
||||
// Timestamp shadows the original Timestamp field and is meant to
|
||||
// remain nil, triggering the omitempty behavior.
|
||||
Timestamp json.RawMessage `json:"timestamp,omitempty"`
|
||||
}{breadcrumb: (*breadcrumb)(b)})
|
||||
}
|
||||
return json.Marshal((*breadcrumb)(b))
|
||||
}
|
||||
|
||||
type Logger interface {
|
||||
// Write implements the io.Writer interface. Currently, the [sentry.Hub] is
|
||||
// context aware, in order to get the correct trace correlation. Using this
|
||||
// might result in incorrect span association on logs. If you need to use
|
||||
// Write it is recommended to create a NewLogger so that the associated context
|
||||
// is passed correctly.
|
||||
Write(p []byte) (n int, err error)
|
||||
// Trace emits a [LogLevelTrace] log to Sentry.
|
||||
// Arguments are handled in the manner of [fmt.Print].
|
||||
Trace(ctx context.Context, v ...interface{})
|
||||
// Debug emits a [LogLevelDebug] log to Sentry.
|
||||
// Arguments are handled in the manner of [fmt.Print].
|
||||
Debug(ctx context.Context, v ...interface{})
|
||||
// Info emits a [LogLevelInfo] log to Sentry.
|
||||
// Arguments are handled in the manner of [fmt.Print].
|
||||
Info(ctx context.Context, v ...interface{})
|
||||
// Warn emits a [LogLevelWarn] log to Sentry.
|
||||
// Arguments are handled in the manner of [fmt.Print].
|
||||
Warn(ctx context.Context, v ...interface{})
|
||||
// Error emits a [LogLevelError] log to Sentry.
|
||||
// Arguments are handled in the manner of [fmt.Print].
|
||||
Error(ctx context.Context, v ...interface{})
|
||||
// Fatal emits a [LogLevelFatal] log to Sentry followed by a call to [os.Exit](1).
|
||||
// Arguments are handled in the manner of [fmt.Print].
|
||||
Fatal(ctx context.Context, v ...interface{})
|
||||
// Panic emits a [LogLevelFatal] log to Sentry followed by a call to panic().
|
||||
// Arguments are handled in the manner of [fmt.Print].
|
||||
Panic(ctx context.Context, v ...interface{})
|
||||
|
||||
// Tracef emits a [LogLevelTrace] log to Sentry.
|
||||
// Arguments are handled in the manner of [fmt.Printf].
|
||||
Tracef(ctx context.Context, format string, v ...interface{})
|
||||
// Debugf emits a [LogLevelDebug] log to Sentry.
|
||||
// Arguments are handled in the manner of [fmt.Printf].
|
||||
Debugf(ctx context.Context, format string, v ...interface{})
|
||||
// Infof emits a [LogLevelInfo] log to Sentry.
|
||||
// Arguments are handled in the manner of [fmt.Printf].
|
||||
Infof(ctx context.Context, format string, v ...interface{})
|
||||
// Warnf emits a [LogLevelWarn] log to Sentry.
|
||||
// Arguments are handled in the manner of [fmt.Printf].
|
||||
Warnf(ctx context.Context, format string, v ...interface{})
|
||||
// Errorf emits a [LogLevelError] log to Sentry.
|
||||
// Arguments are handled in the manner of [fmt.Printf].
|
||||
Errorf(ctx context.Context, format string, v ...interface{})
|
||||
// Fatalf emits a [LogLevelFatal] log to Sentry followed by a call to [os.Exit](1).
|
||||
// Arguments are handled in the manner of [fmt.Printf].
|
||||
Fatalf(ctx context.Context, format string, v ...interface{})
|
||||
// Panicf emits a [LogLevelFatal] log to Sentry followed by a call to panic().
|
||||
// Arguments are handled in the manner of [fmt.Printf].
|
||||
Panicf(ctx context.Context, format string, v ...interface{})
|
||||
// SetAttributes allows attaching parameters to the log message using the attribute API.
|
||||
SetAttributes(...attribute.Builder)
|
||||
}
|
||||
|
||||
// Attachment allows associating files with your events to aid in investigation.
|
||||
// An event may contain one or more attachments.
|
||||
type Attachment struct {
|
||||
Filename string
|
||||
ContentType string
|
||||
Payload []byte
|
||||
}
|
||||
|
||||
// User describes the user associated with an Event. If this is used, at least
|
||||
// an ID or an IP address should be provided.
|
||||
type User struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
IPAddress string `json:"ip_address,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Data map[string]string `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
func (u User) IsEmpty() bool {
|
||||
if u.ID != "" {
|
||||
return false
|
||||
}
|
||||
|
||||
if u.Email != "" {
|
||||
return false
|
||||
}
|
||||
|
||||
if u.IPAddress != "" {
|
||||
return false
|
||||
}
|
||||
|
||||
if u.Username != "" {
|
||||
return false
|
||||
}
|
||||
|
||||
if u.Name != "" {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(u.Data) > 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Request contains information on a HTTP request related to the event.
|
||||
type Request struct {
|
||||
URL string `json:"url,omitempty"`
|
||||
Method string `json:"method,omitempty"`
|
||||
Data string `json:"data,omitempty"`
|
||||
QueryString string `json:"query_string,omitempty"`
|
||||
Cookies string `json:"cookies,omitempty"`
|
||||
Headers map[string]string `json:"headers,omitempty"`
|
||||
Env map[string]string `json:"env,omitempty"`
|
||||
}
|
||||
|
||||
var sensitiveHeaders = map[string]struct{}{
|
||||
"Authorization": {},
|
||||
"Proxy-Authorization": {},
|
||||
"Cookie": {},
|
||||
"X-Forwarded-For": {},
|
||||
"X-Real-Ip": {},
|
||||
}
|
||||
|
||||
// NewRequest returns a new Sentry Request from the given http.Request.
|
||||
//
|
||||
// NewRequest avoids operations that depend on network access. In particular, it
|
||||
// does not read r.Body.
|
||||
func NewRequest(r *http.Request) *Request {
|
||||
protocol := schemeHTTP
|
||||
if r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" {
|
||||
protocol = schemeHTTPS
|
||||
}
|
||||
url := fmt.Sprintf("%s://%s%s", protocol, r.Host, r.URL.Path)
|
||||
|
||||
var cookies string
|
||||
var env map[string]string
|
||||
headers := map[string]string{}
|
||||
|
||||
if client := CurrentHub().Client(); client != nil && client.options.SendDefaultPII {
|
||||
// We read only the first Cookie header because of the specification:
|
||||
// https://tools.ietf.org/html/rfc6265#section-5.4
|
||||
// When the user agent generates an HTTP request, the user agent MUST NOT
|
||||
// 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, ",")
|
||||
}
|
||||
|
||||
if addr, port, err := net.SplitHostPort(r.RemoteAddr); err == nil {
|
||||
env = map[string]string{"REMOTE_ADDR": addr, "REMOTE_PORT": port}
|
||||
}
|
||||
} else {
|
||||
for k, v := range r.Header {
|
||||
if _, ok := sensitiveHeaders[k]; !ok {
|
||||
headers[k] = strings.Join(v, ",")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
headers["Host"] = r.Host
|
||||
|
||||
return &Request{
|
||||
URL: url,
|
||||
Method: r.Method,
|
||||
QueryString: r.URL.RawQuery,
|
||||
Cookies: cookies,
|
||||
Headers: headers,
|
||||
Env: env,
|
||||
}
|
||||
}
|
||||
|
||||
// Mechanism is the mechanism by which an exception was generated and handled.
|
||||
type Mechanism struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
HelpLink string `json:"help_link,omitempty"`
|
||||
Source string `json:"source,omitempty"`
|
||||
Handled *bool `json:"handled,omitempty"`
|
||||
ParentID *int `json:"parent_id,omitempty"`
|
||||
ExceptionID int `json:"exception_id"`
|
||||
IsExceptionGroup bool `json:"is_exception_group,omitempty"`
|
||||
Data map[string]any `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// SetUnhandled indicates that the exception is an unhandled exception, i.e.
|
||||
// from a panic.
|
||||
func (m *Mechanism) SetUnhandled() {
|
||||
m.Handled = Pointer(false)
|
||||
}
|
||||
|
||||
// Exception specifies an error that occurred.
|
||||
type Exception struct {
|
||||
Type string `json:"type,omitempty"` // used as the main issue title
|
||||
Value string `json:"value,omitempty"` // used as the main issue subtitle
|
||||
Module string `json:"module,omitempty"`
|
||||
ThreadID uint64 `json:"thread_id,omitempty"`
|
||||
Stacktrace *Stacktrace `json:"stacktrace,omitempty"`
|
||||
Mechanism *Mechanism `json:"mechanism,omitempty"`
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Contains information about how the name of the transaction was determined.
|
||||
type TransactionInfo struct {
|
||||
Source TransactionSource `json:"source,omitempty"`
|
||||
}
|
||||
|
||||
// The DebugMeta interface is not used in Golang apps, but may be populated
|
||||
// when proxying Events from other platforms, like iOS, Android, and the
|
||||
// Web. (See: https://develop.sentry.dev/sdk/event-payloads/debugmeta/ ).
|
||||
type DebugMeta struct {
|
||||
SdkInfo *DebugMetaSdkInfo `json:"sdk_info,omitempty"`
|
||||
Images []DebugMetaImage `json:"images,omitempty"`
|
||||
}
|
||||
|
||||
type DebugMetaSdkInfo struct {
|
||||
SdkName string `json:"sdk_name,omitempty"`
|
||||
VersionMajor int `json:"version_major,omitempty"`
|
||||
VersionMinor int `json:"version_minor,omitempty"`
|
||||
VersionPatchlevel int `json:"version_patchlevel,omitempty"`
|
||||
}
|
||||
|
||||
type DebugMetaImage struct {
|
||||
Type string `json:"type,omitempty"` // all
|
||||
ImageAddr string `json:"image_addr,omitempty"` // macho,elf,pe
|
||||
ImageSize int `json:"image_size,omitempty"` // macho,elf,pe
|
||||
DebugID string `json:"debug_id,omitempty"` // macho,elf,pe,wasm,sourcemap
|
||||
DebugFile string `json:"debug_file,omitempty"` // macho,elf,pe,wasm
|
||||
CodeID string `json:"code_id,omitempty"` // macho,elf,pe,wasm
|
||||
CodeFile string `json:"code_file,omitempty"` // macho,elf,pe,wasm,sourcemap
|
||||
ImageVmaddr string `json:"image_vmaddr,omitempty"` // macho,elf,pe
|
||||
Arch string `json:"arch,omitempty"` // macho,elf,pe
|
||||
UUID string `json:"uuid,omitempty"` // proguard
|
||||
}
|
||||
|
||||
// EventID is a hexadecimal string representing a unique uuid4 for an Event.
|
||||
// An EventID must be 32 characters long, lowercase and not have any dashes.
|
||||
type EventID string
|
||||
|
||||
type Context = map[string]interface{}
|
||||
|
||||
// Event is the fundamental data structure that is sent to Sentry.
|
||||
type Event struct {
|
||||
Breadcrumbs []*Breadcrumb `json:"breadcrumbs,omitempty"`
|
||||
Contexts map[string]Context `json:"contexts,omitempty"`
|
||||
Dist string `json:"dist,omitempty"`
|
||||
Environment string `json:"environment,omitempty"`
|
||||
EventID EventID `json:"event_id,omitempty"`
|
||||
Extra map[string]interface{} `json:"extra,omitempty"`
|
||||
Fingerprint []string `json:"fingerprint,omitempty"`
|
||||
Level Level `json:"level,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Platform string `json:"platform,omitempty"`
|
||||
Release string `json:"release,omitempty"`
|
||||
Sdk SdkInfo `json:"sdk,omitempty"`
|
||||
ServerName string `json:"server_name,omitempty"`
|
||||
Threads []Thread `json:"threads,omitempty"`
|
||||
Tags map[string]string `json:"tags,omitempty"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Transaction string `json:"transaction,omitempty"`
|
||||
User User `json:"user,omitempty"`
|
||||
Logger string `json:"logger,omitempty"`
|
||||
Modules map[string]string `json:"modules,omitempty"`
|
||||
Request *Request `json:"request,omitempty"`
|
||||
Exception []Exception `json:"exception,omitempty"`
|
||||
DebugMeta *DebugMeta `json:"debug_meta,omitempty"`
|
||||
Attachments []*Attachment `json:"-"`
|
||||
|
||||
// The fields below are only relevant for transactions.
|
||||
|
||||
Type string `json:"type,omitempty"`
|
||||
StartTime time.Time `json:"start_timestamp"`
|
||||
Spans []*Span `json:"spans,omitempty"`
|
||||
TransactionInfo *TransactionInfo `json:"transaction_info,omitempty"`
|
||||
|
||||
// The fields below are only relevant for crons/check ins
|
||||
|
||||
CheckIn *CheckIn `json:"check_in,omitempty"`
|
||||
MonitorConfig *MonitorConfig `json:"monitor_config,omitempty"`
|
||||
|
||||
// The fields below are only relevant for logs
|
||||
Logs []Log `json:"items,omitempty"`
|
||||
|
||||
// The fields below are not part of the final JSON payload.
|
||||
|
||||
sdkMetaData SDKMetaData
|
||||
}
|
||||
|
||||
// SetException appends the unwrapped errors to the event's exception list.
|
||||
//
|
||||
// maxErrorDepth is the maximum depth of the error chain we will look
|
||||
// into while unwrapping the errors. If maxErrorDepth is -1, we will
|
||||
// unwrap all errors in the chain.
|
||||
func (e *Event) SetException(exception error, maxErrorDepth int) {
|
||||
if exception == nil {
|
||||
return
|
||||
}
|
||||
|
||||
err := exception
|
||||
|
||||
for i := 0; err != nil && (i < maxErrorDepth || maxErrorDepth == -1); i++ {
|
||||
// Add the current error to the exception slice with its details
|
||||
e.Exception = append(e.Exception, Exception{
|
||||
Value: err.Error(),
|
||||
Type: reflect.TypeOf(err).String(),
|
||||
Stacktrace: ExtractStacktrace(err),
|
||||
})
|
||||
|
||||
// Attempt to unwrap the error using the standard library's Unwrap method.
|
||||
// If errors.Unwrap returns nil, it means either there is no error to unwrap,
|
||||
// or the error does not implement the Unwrap method.
|
||||
unwrappedErr := errors.Unwrap(err)
|
||||
|
||||
if unwrappedErr != nil {
|
||||
// The error was successfully unwrapped using the standard library's Unwrap method.
|
||||
err = unwrappedErr
|
||||
continue
|
||||
}
|
||||
|
||||
cause, ok := err.(interface{ Cause() error })
|
||||
if !ok {
|
||||
// We cannot unwrap the error further.
|
||||
break
|
||||
}
|
||||
|
||||
// The error implements the Cause method, indicating it may have been wrapped
|
||||
// using the github.com/pkg/errors package.
|
||||
err = cause.Cause()
|
||||
}
|
||||
|
||||
// Add a trace of the current stack to the most recent error in a chain if
|
||||
// it doesn't have a stack trace yet.
|
||||
// We only add to the most recent error to avoid duplication and because the
|
||||
// current stack is most likely unrelated to errors deeper in the chain.
|
||||
if e.Exception[0].Stacktrace == nil {
|
||||
e.Exception[0].Stacktrace = NewStacktrace()
|
||||
}
|
||||
|
||||
if len(e.Exception) <= 1 {
|
||||
return
|
||||
}
|
||||
|
||||
// event.Exception should be sorted such that the most recent error is last.
|
||||
slices.Reverse(e.Exception)
|
||||
|
||||
for i := range e.Exception {
|
||||
e.Exception[i].Mechanism = &Mechanism{
|
||||
IsExceptionGroup: true,
|
||||
ExceptionID: i,
|
||||
Type: "generic",
|
||||
}
|
||||
if i == 0 {
|
||||
continue
|
||||
}
|
||||
e.Exception[i].Mechanism.ParentID = Pointer(i - 1)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Event.Contexts map[string]interface{} => map[string]EventContext,
|
||||
// to prevent accidentally storing T when we mean *T.
|
||||
// For example, the TraceContext must be stored as *TraceContext to pick up the
|
||||
// MarshalJSON method (and avoid copying).
|
||||
// type EventContext interface{ EventContext() }
|
||||
|
||||
// MarshalJSON converts the Event struct to JSON.
|
||||
func (e *Event) MarshalJSON() ([]byte, error) {
|
||||
// We want to omit time.Time zero values, otherwise the server will try to
|
||||
// interpret dates too far in the past. However, encoding/json doesn't
|
||||
// support the "omitempty" option for struct types. See
|
||||
// https://golang.org/issues/11939.
|
||||
//
|
||||
// We overcome the limitation and achieve what we want by shadowing fields
|
||||
// and a few type tricks.
|
||||
if e.Type == transactionType {
|
||||
return e.transactionMarshalJSON()
|
||||
}
|
||||
|
||||
if e.Type == checkInType {
|
||||
return e.checkInMarshalJSON()
|
||||
}
|
||||
return e.defaultMarshalJSON()
|
||||
}
|
||||
|
||||
func (e *Event) defaultMarshalJSON() ([]byte, error) {
|
||||
// event aliases Event to allow calling json.Marshal without an infinite
|
||||
// loop. It preserves all fields while none of the attached methods.
|
||||
type event Event
|
||||
|
||||
// errorEvent is like Event with shadowed fields for customizing JSON
|
||||
// marshaling.
|
||||
type errorEvent struct {
|
||||
*event
|
||||
|
||||
// Timestamp shadows the original Timestamp field. It allows us to
|
||||
// include the timestamp when non-zero and omit it otherwise.
|
||||
Timestamp json.RawMessage `json:"timestamp,omitempty"`
|
||||
|
||||
// The fields below are not part of error events and only make sense to
|
||||
// be sent for transactions. They shadow the respective fields in Event
|
||||
// and are meant to remain nil, triggering the omitempty behavior.
|
||||
|
||||
Type json.RawMessage `json:"type,omitempty"`
|
||||
StartTime json.RawMessage `json:"start_timestamp,omitempty"`
|
||||
Spans json.RawMessage `json:"spans,omitempty"`
|
||||
TransactionInfo json.RawMessage `json:"transaction_info,omitempty"`
|
||||
}
|
||||
|
||||
x := errorEvent{event: (*event)(e)}
|
||||
if !e.Timestamp.IsZero() {
|
||||
b, err := e.Timestamp.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
x.Timestamp = b
|
||||
}
|
||||
return json.Marshal(x)
|
||||
}
|
||||
|
||||
func (e *Event) transactionMarshalJSON() ([]byte, error) {
|
||||
// event aliases Event to allow calling json.Marshal without an infinite
|
||||
// loop. It preserves all fields while none of the attached methods.
|
||||
type event Event
|
||||
|
||||
// transactionEvent is like Event with shadowed fields for customizing JSON
|
||||
// marshaling.
|
||||
type transactionEvent struct {
|
||||
*event
|
||||
|
||||
// The fields below shadow the respective fields in Event. They allow us
|
||||
// to include timestamps when non-zero and omit them otherwise.
|
||||
|
||||
StartTime json.RawMessage `json:"start_timestamp,omitempty"`
|
||||
Timestamp json.RawMessage `json:"timestamp,omitempty"`
|
||||
}
|
||||
|
||||
x := transactionEvent{event: (*event)(e)}
|
||||
if !e.Timestamp.IsZero() {
|
||||
b, err := e.Timestamp.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
x.Timestamp = b
|
||||
}
|
||||
if !e.StartTime.IsZero() {
|
||||
b, err := e.StartTime.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
x.StartTime = b
|
||||
}
|
||||
return json.Marshal(x)
|
||||
}
|
||||
|
||||
func (e *Event) checkInMarshalJSON() ([]byte, error) {
|
||||
checkIn := serializedCheckIn{
|
||||
CheckInID: string(e.CheckIn.ID),
|
||||
MonitorSlug: e.CheckIn.MonitorSlug,
|
||||
Status: e.CheckIn.Status,
|
||||
Duration: e.CheckIn.Duration.Seconds(),
|
||||
Release: e.Release,
|
||||
Environment: e.Environment,
|
||||
MonitorConfig: nil,
|
||||
}
|
||||
|
||||
if e.MonitorConfig != nil {
|
||||
checkIn.MonitorConfig = &MonitorConfig{
|
||||
Schedule: e.MonitorConfig.Schedule,
|
||||
CheckInMargin: e.MonitorConfig.CheckInMargin,
|
||||
MaxRuntime: e.MonitorConfig.MaxRuntime,
|
||||
Timezone: e.MonitorConfig.Timezone,
|
||||
}
|
||||
}
|
||||
|
||||
return json.Marshal(checkIn)
|
||||
}
|
||||
|
||||
// NewEvent creates a new Event.
|
||||
func NewEvent() *Event {
|
||||
return &Event{
|
||||
Contexts: make(map[string]Context),
|
||||
Extra: make(map[string]interface{}),
|
||||
Tags: make(map[string]string),
|
||||
Modules: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
// Thread specifies threads that were running at the time of an event.
|
||||
type Thread struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Stacktrace *Stacktrace `json:"stacktrace,omitempty"`
|
||||
Crashed bool `json:"crashed,omitempty"`
|
||||
Current bool `json:"current,omitempty"`
|
||||
}
|
||||
|
||||
// EventHint contains information that can be associated with an Event.
|
||||
type EventHint struct {
|
||||
Data interface{}
|
||||
EventID string
|
||||
OriginalException error
|
||||
RecoveredException interface{}
|
||||
Context context.Context
|
||||
Request *http.Request
|
||||
Response *http.Response
|
||||
}
|
||||
|
||||
type Log struct {
|
||||
Timestamp time.Time `json:"timestamp,omitempty"`
|
||||
TraceID TraceID `json:"trace_id,omitempty"`
|
||||
Level Level `json:"level"`
|
||||
Severity int `json:"severity_number,omitempty"`
|
||||
Body string `json:"body,omitempty"`
|
||||
Attributes map[string]Attribute `json:"attributes,omitempty"`
|
||||
}
|
||||
|
||||
type Attribute struct {
|
||||
Value any `json:"value"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
79
vendor/github.com/getsentry/sentry-go/internal/debug/transport.go
generated
vendored
Normal file
79
vendor/github.com/getsentry/sentry-go/internal/debug/transport.go
generated
vendored
Normal file
@@ -0,0 +1,79 @@
|
||||
package debug
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptrace"
|
||||
"net/http/httputil"
|
||||
)
|
||||
|
||||
// Transport implements http.RoundTripper and can be used to wrap other HTTP
|
||||
// transports for debugging, normally http.DefaultTransport.
|
||||
type Transport struct {
|
||||
http.RoundTripper
|
||||
Output io.Writer
|
||||
// Dump controls whether to dump HTTP request and responses.
|
||||
Dump bool
|
||||
// Trace enables usage of net/http/httptrace.
|
||||
Trace bool
|
||||
}
|
||||
|
||||
func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
var buf bytes.Buffer
|
||||
if t.Dump {
|
||||
b, err := httputil.DumpRequestOut(req, true)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
_, err = buf.Write(ensureTrailingNewline(b))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
if t.Trace {
|
||||
trace := &httptrace.ClientTrace{
|
||||
DNSDone: func(di httptrace.DNSDoneInfo) {
|
||||
fmt.Fprintf(&buf, "* DNS %v → %v\n", req.Host, di.Addrs)
|
||||
},
|
||||
GotConn: func(ci httptrace.GotConnInfo) {
|
||||
fmt.Fprintf(&buf, "* Connection local=%v remote=%v", ci.Conn.LocalAddr(), ci.Conn.RemoteAddr())
|
||||
if ci.Reused {
|
||||
fmt.Fprint(&buf, " (reused)")
|
||||
}
|
||||
if ci.WasIdle {
|
||||
fmt.Fprintf(&buf, " (idle %v)", ci.IdleTime)
|
||||
}
|
||||
fmt.Fprintln(&buf)
|
||||
},
|
||||
}
|
||||
req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))
|
||||
}
|
||||
resp, err := t.RoundTripper.RoundTrip(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if t.Dump {
|
||||
b, err := httputil.DumpResponse(resp, true)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
_, err = buf.Write(ensureTrailingNewline(b))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
_, err = io.Copy(t.Output, &buf)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func ensureTrailingNewline(b []byte) []byte {
|
||||
if len(b) > 0 && b[len(b)-1] != '\n' {
|
||||
b = append(b, '\n')
|
||||
}
|
||||
return b
|
||||
}
|
||||
12
vendor/github.com/getsentry/sentry-go/internal/otel/baggage/README.md
generated
vendored
Normal file
12
vendor/github.com/getsentry/sentry-go/internal/otel/baggage/README.md
generated
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
## Why do we have this "otel/baggage" folder?
|
||||
|
||||
The root sentry-go SDK (namely, the Dynamic Sampling functionality) needs an implementation of the [baggage spec](https://www.w3.org/TR/baggage/).
|
||||
For that reason, we've taken the existing baggage implementation from the [opentelemetry-go](https://github.com/open-telemetry/opentelemetry-go/) repository, and fixed a few things that in our opinion were violating the specification.
|
||||
|
||||
These issues are:
|
||||
1. Baggage string value `one%20two` should be properly parsed as "one two"
|
||||
1. Baggage string value `one+two` should be parsed as "one+two"
|
||||
1. Go string value "one two" should be encoded as `one%20two` (percent encoding), and NOT as `one+two` (URL query encoding).
|
||||
1. Go string value "1=1" might be encoded as `1=1`, because the spec says: "Note, value MAY contain any number of the equal sign (=) characters. Parsers MUST NOT assume that the equal sign is only used to separate key and value.". `1%3D1` is also valid, but to simplify the implementation we're not doing it.
|
||||
|
||||
Changes were made in this PR: https://github.com/getsentry/sentry-go/pull/568
|
||||
604
vendor/github.com/getsentry/sentry-go/internal/otel/baggage/baggage.go
generated
vendored
Normal file
604
vendor/github.com/getsentry/sentry-go/internal/otel/baggage/baggage.go
generated
vendored
Normal file
@@ -0,0 +1,604 @@
|
||||
// Adapted from https://github.com/open-telemetry/opentelemetry-go/blob/c21b6b6bb31a2f74edd06e262f1690f3f6ea3d5c/baggage/baggage.go
|
||||
//
|
||||
// Copyright The OpenTelemetry Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package baggage
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/getsentry/sentry-go/internal/otel/baggage/internal/baggage"
|
||||
)
|
||||
|
||||
const (
|
||||
maxMembers = 180
|
||||
maxBytesPerMembers = 4096
|
||||
maxBytesPerBaggageString = 8192
|
||||
|
||||
listDelimiter = ","
|
||||
keyValueDelimiter = "="
|
||||
propertyDelimiter = ";"
|
||||
|
||||
keyDef = `([\x21\x23-\x27\x2A\x2B\x2D\x2E\x30-\x39\x41-\x5a\x5e-\x7a\x7c\x7e]+)`
|
||||
valueDef = `([\x21\x23-\x2b\x2d-\x3a\x3c-\x5B\x5D-\x7e]*)`
|
||||
keyValueDef = `\s*` + keyDef + `\s*` + keyValueDelimiter + `\s*` + valueDef + `\s*`
|
||||
)
|
||||
|
||||
var (
|
||||
keyRe = regexp.MustCompile(`^` + keyDef + `$`)
|
||||
valueRe = regexp.MustCompile(`^` + valueDef + `$`)
|
||||
propertyRe = regexp.MustCompile(`^(?:\s*` + keyDef + `\s*|` + keyValueDef + `)$`)
|
||||
)
|
||||
|
||||
var (
|
||||
errInvalidKey = errors.New("invalid key")
|
||||
errInvalidValue = errors.New("invalid value")
|
||||
errInvalidProperty = errors.New("invalid baggage list-member property")
|
||||
errInvalidMember = errors.New("invalid baggage list-member")
|
||||
errMemberNumber = errors.New("too many list-members in baggage-string")
|
||||
errMemberBytes = errors.New("list-member too large")
|
||||
errBaggageBytes = errors.New("baggage-string too large")
|
||||
)
|
||||
|
||||
// Property is an additional metadata entry for a baggage list-member.
|
||||
type Property struct {
|
||||
key, value string
|
||||
|
||||
// hasValue indicates if a zero-value value means the property does not
|
||||
// have a value or if it was the zero-value.
|
||||
hasValue bool
|
||||
|
||||
// hasData indicates whether the created property contains data or not.
|
||||
// Properties that do not contain data are invalid with no other check
|
||||
// required.
|
||||
hasData bool
|
||||
}
|
||||
|
||||
// NewKeyProperty returns a new Property for key.
|
||||
//
|
||||
// If key is invalid, an error will be returned.
|
||||
func NewKeyProperty(key string) (Property, error) {
|
||||
if !keyRe.MatchString(key) {
|
||||
return newInvalidProperty(), fmt.Errorf("%w: %q", errInvalidKey, key)
|
||||
}
|
||||
|
||||
p := Property{key: key, hasData: true}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// NewKeyValueProperty returns a new Property for key with value.
|
||||
//
|
||||
// If key or value are invalid, an error will be returned.
|
||||
func NewKeyValueProperty(key, value string) (Property, error) {
|
||||
if !keyRe.MatchString(key) {
|
||||
return newInvalidProperty(), fmt.Errorf("%w: %q", errInvalidKey, key)
|
||||
}
|
||||
if !valueRe.MatchString(value) {
|
||||
return newInvalidProperty(), fmt.Errorf("%w: %q", errInvalidValue, value)
|
||||
}
|
||||
|
||||
p := Property{
|
||||
key: key,
|
||||
value: value,
|
||||
hasValue: true,
|
||||
hasData: true,
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func newInvalidProperty() Property {
|
||||
return Property{}
|
||||
}
|
||||
|
||||
// parseProperty attempts to decode a Property from the passed string. It
|
||||
// returns an error if the input is invalid according to the W3C Baggage
|
||||
// specification.
|
||||
func parseProperty(property string) (Property, error) {
|
||||
if property == "" {
|
||||
return newInvalidProperty(), nil
|
||||
}
|
||||
|
||||
match := propertyRe.FindStringSubmatch(property)
|
||||
if len(match) != 4 {
|
||||
return newInvalidProperty(), fmt.Errorf("%w: %q", errInvalidProperty, property)
|
||||
}
|
||||
|
||||
p := Property{hasData: true}
|
||||
if match[1] != "" {
|
||||
p.key = match[1]
|
||||
} else {
|
||||
p.key = match[2]
|
||||
p.value = match[3]
|
||||
p.hasValue = true
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// validate ensures p conforms to the W3C Baggage specification, returning an
|
||||
// error otherwise.
|
||||
func (p Property) validate() error {
|
||||
errFunc := func(err error) error {
|
||||
return fmt.Errorf("invalid property: %w", err)
|
||||
}
|
||||
|
||||
if !p.hasData {
|
||||
return errFunc(fmt.Errorf("%w: %q", errInvalidProperty, p))
|
||||
}
|
||||
|
||||
if !keyRe.MatchString(p.key) {
|
||||
return errFunc(fmt.Errorf("%w: %q", errInvalidKey, p.key))
|
||||
}
|
||||
if p.hasValue && !valueRe.MatchString(p.value) {
|
||||
return errFunc(fmt.Errorf("%w: %q", errInvalidValue, p.value))
|
||||
}
|
||||
if !p.hasValue && p.value != "" {
|
||||
return errFunc(errors.New("inconsistent value"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Key returns the Property key.
|
||||
func (p Property) Key() string {
|
||||
return p.key
|
||||
}
|
||||
|
||||
// Value returns the Property value. Additionally, a boolean value is returned
|
||||
// indicating if the returned value is the empty if the Property has a value
|
||||
// that is empty or if the value is not set.
|
||||
func (p Property) Value() (string, bool) {
|
||||
return p.value, p.hasValue
|
||||
}
|
||||
|
||||
// String encodes Property into a string compliant with the W3C Baggage
|
||||
// specification.
|
||||
func (p Property) String() string {
|
||||
if p.hasValue {
|
||||
return fmt.Sprintf("%s%s%v", p.key, keyValueDelimiter, p.value)
|
||||
}
|
||||
return p.key
|
||||
}
|
||||
|
||||
type properties []Property
|
||||
|
||||
func fromInternalProperties(iProps []baggage.Property) properties {
|
||||
if len(iProps) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
props := make(properties, len(iProps))
|
||||
for i, p := range iProps {
|
||||
props[i] = Property{
|
||||
key: p.Key,
|
||||
value: p.Value,
|
||||
hasValue: p.HasValue,
|
||||
}
|
||||
}
|
||||
return props
|
||||
}
|
||||
|
||||
func (p properties) asInternal() []baggage.Property {
|
||||
if len(p) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
iProps := make([]baggage.Property, len(p))
|
||||
for i, prop := range p {
|
||||
iProps[i] = baggage.Property{
|
||||
Key: prop.key,
|
||||
Value: prop.value,
|
||||
HasValue: prop.hasValue,
|
||||
}
|
||||
}
|
||||
return iProps
|
||||
}
|
||||
|
||||
func (p properties) Copy() properties {
|
||||
if len(p) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
props := make(properties, len(p))
|
||||
copy(props, p)
|
||||
return props
|
||||
}
|
||||
|
||||
// validate ensures each Property in p conforms to the W3C Baggage
|
||||
// specification, returning an error otherwise.
|
||||
func (p properties) validate() error {
|
||||
for _, prop := range p {
|
||||
if err := prop.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// String encodes properties into a string compliant with the W3C Baggage
|
||||
// specification.
|
||||
func (p properties) String() string {
|
||||
props := make([]string, len(p))
|
||||
for i, prop := range p {
|
||||
props[i] = prop.String()
|
||||
}
|
||||
return strings.Join(props, propertyDelimiter)
|
||||
}
|
||||
|
||||
// Member is a list-member of a baggage-string as defined by the W3C Baggage
|
||||
// specification.
|
||||
type Member struct {
|
||||
key, value string
|
||||
properties properties
|
||||
|
||||
// hasData indicates whether the created property contains data or not.
|
||||
// Properties that do not contain data are invalid with no other check
|
||||
// required.
|
||||
hasData bool
|
||||
}
|
||||
|
||||
// NewMember returns a new Member from the passed arguments. The key will be
|
||||
// used directly while the value will be url decoded after validation. An error
|
||||
// is returned if the created Member would be invalid according to the W3C
|
||||
// Baggage specification.
|
||||
func NewMember(key, value string, props ...Property) (Member, error) {
|
||||
m := Member{
|
||||
key: key,
|
||||
value: value,
|
||||
properties: properties(props).Copy(),
|
||||
hasData: true,
|
||||
}
|
||||
if err := m.validate(); err != nil {
|
||||
return newInvalidMember(), err
|
||||
}
|
||||
//// NOTE(anton): I don't think we need to unescape here
|
||||
// decodedValue, err := url.PathUnescape(value)
|
||||
// if err != nil {
|
||||
// return newInvalidMember(), fmt.Errorf("%w: %q", errInvalidValue, value)
|
||||
// }
|
||||
// m.value = decodedValue
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func newInvalidMember() Member {
|
||||
return Member{}
|
||||
}
|
||||
|
||||
// parseMember attempts to decode a Member from the passed string. It returns
|
||||
// an error if the input is invalid according to the W3C Baggage
|
||||
// specification.
|
||||
func parseMember(member string) (Member, error) {
|
||||
if n := len(member); n > maxBytesPerMembers {
|
||||
return newInvalidMember(), fmt.Errorf("%w: %d", errMemberBytes, n)
|
||||
}
|
||||
|
||||
var (
|
||||
key, value string
|
||||
props properties
|
||||
)
|
||||
|
||||
parts := strings.SplitN(member, propertyDelimiter, 2)
|
||||
switch len(parts) {
|
||||
case 2:
|
||||
// Parse the member properties.
|
||||
for _, pStr := range strings.Split(parts[1], propertyDelimiter) {
|
||||
p, err := parseProperty(pStr)
|
||||
if err != nil {
|
||||
return newInvalidMember(), err
|
||||
}
|
||||
props = append(props, p)
|
||||
}
|
||||
fallthrough
|
||||
case 1:
|
||||
// Parse the member key/value pair.
|
||||
|
||||
// Take into account a value can contain equal signs (=).
|
||||
kv := strings.SplitN(parts[0], keyValueDelimiter, 2)
|
||||
if len(kv) != 2 {
|
||||
return newInvalidMember(), fmt.Errorf("%w: %q", errInvalidMember, member)
|
||||
}
|
||||
// "Leading and trailing whitespaces are allowed but MUST be trimmed
|
||||
// when converting the header into a data structure."
|
||||
key = strings.TrimSpace(kv[0])
|
||||
value = strings.TrimSpace(kv[1])
|
||||
var err error
|
||||
if !keyRe.MatchString(key) {
|
||||
return newInvalidMember(), fmt.Errorf("%w: %q", errInvalidKey, key)
|
||||
}
|
||||
if !valueRe.MatchString(value) {
|
||||
return newInvalidMember(), fmt.Errorf("%w: %q", errInvalidValue, value)
|
||||
}
|
||||
decodedValue, err := url.PathUnescape(value)
|
||||
if err != nil {
|
||||
return newInvalidMember(), fmt.Errorf("%w: %q", err, value)
|
||||
}
|
||||
value = decodedValue
|
||||
default:
|
||||
// This should never happen unless a developer has changed the string
|
||||
// splitting somehow. Panic instead of failing silently and allowing
|
||||
// the bug to slip past the CI checks.
|
||||
panic("failed to parse baggage member")
|
||||
}
|
||||
|
||||
return Member{key: key, value: value, properties: props, hasData: true}, nil
|
||||
}
|
||||
|
||||
// validate ensures m conforms to the W3C Baggage specification.
|
||||
// A key is just an ASCII string, but a value must be URL encoded UTF-8,
|
||||
// returning an error otherwise.
|
||||
func (m Member) validate() error {
|
||||
if !m.hasData {
|
||||
return fmt.Errorf("%w: %q", errInvalidMember, m)
|
||||
}
|
||||
|
||||
if !keyRe.MatchString(m.key) {
|
||||
return fmt.Errorf("%w: %q", errInvalidKey, m.key)
|
||||
}
|
||||
//// NOTE(anton): IMO it's too early to validate the value here.
|
||||
// if !valueRe.MatchString(m.value) {
|
||||
// return fmt.Errorf("%w: %q", errInvalidValue, m.value)
|
||||
// }
|
||||
return m.properties.validate()
|
||||
}
|
||||
|
||||
// Key returns the Member key.
|
||||
func (m Member) Key() string { return m.key }
|
||||
|
||||
// Value returns the Member value.
|
||||
func (m Member) Value() string { return m.value }
|
||||
|
||||
// Properties returns a copy of the Member properties.
|
||||
func (m Member) Properties() []Property { return m.properties.Copy() }
|
||||
|
||||
// String encodes Member into a string compliant with the W3C Baggage
|
||||
// specification.
|
||||
func (m Member) String() string {
|
||||
// A key is just an ASCII string, but a value is URL encoded UTF-8.
|
||||
s := fmt.Sprintf("%s%s%s", m.key, keyValueDelimiter, percentEncodeValue(m.value))
|
||||
if len(m.properties) > 0 {
|
||||
s = fmt.Sprintf("%s%s%s", s, propertyDelimiter, m.properties.String())
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// percentEncodeValue encodes the baggage value, using percent-encoding for
|
||||
// disallowed octets.
|
||||
func percentEncodeValue(s string) string {
|
||||
const upperhex = "0123456789ABCDEF"
|
||||
var sb strings.Builder
|
||||
|
||||
for byteIndex, width := 0, 0; byteIndex < len(s); byteIndex += width {
|
||||
runeValue, w := utf8.DecodeRuneInString(s[byteIndex:])
|
||||
width = w
|
||||
char := string(runeValue)
|
||||
if valueRe.MatchString(char) && char != "%" {
|
||||
// The character is returned as is, no need to percent-encode
|
||||
sb.WriteString(char)
|
||||
} else {
|
||||
// We need to percent-encode each byte of the multi-octet character
|
||||
for j := 0; j < width; j++ {
|
||||
b := s[byteIndex+j]
|
||||
sb.WriteByte('%')
|
||||
// Bitwise operations are inspired by "net/url"
|
||||
sb.WriteByte(upperhex[b>>4])
|
||||
sb.WriteByte(upperhex[b&15])
|
||||
}
|
||||
}
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// Baggage is a list of baggage members representing the baggage-string as
|
||||
// defined by the W3C Baggage specification.
|
||||
type Baggage struct { //nolint:golint
|
||||
list baggage.List
|
||||
}
|
||||
|
||||
// New returns a new valid Baggage. It returns an error if it results in a
|
||||
// Baggage exceeding limits set in that specification.
|
||||
//
|
||||
// It expects all the provided members to have already been validated.
|
||||
func New(members ...Member) (Baggage, error) {
|
||||
if len(members) == 0 {
|
||||
return Baggage{}, nil
|
||||
}
|
||||
|
||||
b := make(baggage.List)
|
||||
for _, m := range members {
|
||||
if !m.hasData {
|
||||
return Baggage{}, errInvalidMember
|
||||
}
|
||||
|
||||
// OpenTelemetry resolves duplicates by last-one-wins.
|
||||
b[m.key] = baggage.Item{
|
||||
Value: m.value,
|
||||
Properties: m.properties.asInternal(),
|
||||
}
|
||||
}
|
||||
|
||||
// Check member numbers after deduplication.
|
||||
if len(b) > maxMembers {
|
||||
return Baggage{}, errMemberNumber
|
||||
}
|
||||
|
||||
bag := Baggage{b}
|
||||
if n := len(bag.String()); n > maxBytesPerBaggageString {
|
||||
return Baggage{}, fmt.Errorf("%w: %d", errBaggageBytes, n)
|
||||
}
|
||||
|
||||
return bag, nil
|
||||
}
|
||||
|
||||
// Parse attempts to decode a baggage-string from the passed string. It
|
||||
// returns an error if the input is invalid according to the W3C Baggage
|
||||
// specification.
|
||||
//
|
||||
// If there are duplicate list-members contained in baggage, the last one
|
||||
// defined (reading left-to-right) will be the only one kept. This diverges
|
||||
// from the W3C Baggage specification which allows duplicate list-members, but
|
||||
// conforms to the OpenTelemetry Baggage specification.
|
||||
func Parse(bStr string) (Baggage, error) {
|
||||
if bStr == "" {
|
||||
return Baggage{}, nil
|
||||
}
|
||||
|
||||
if n := len(bStr); n > maxBytesPerBaggageString {
|
||||
return Baggage{}, fmt.Errorf("%w: %d", errBaggageBytes, n)
|
||||
}
|
||||
|
||||
b := make(baggage.List)
|
||||
for _, memberStr := range strings.Split(bStr, listDelimiter) {
|
||||
m, err := parseMember(memberStr)
|
||||
if err != nil {
|
||||
return Baggage{}, err
|
||||
}
|
||||
// OpenTelemetry resolves duplicates by last-one-wins.
|
||||
b[m.key] = baggage.Item{
|
||||
Value: m.value,
|
||||
Properties: m.properties.asInternal(),
|
||||
}
|
||||
}
|
||||
|
||||
// OpenTelemetry does not allow for duplicate list-members, but the W3C
|
||||
// specification does. Now that we have deduplicated, ensure the baggage
|
||||
// does not exceed list-member limits.
|
||||
if len(b) > maxMembers {
|
||||
return Baggage{}, errMemberNumber
|
||||
}
|
||||
|
||||
return Baggage{b}, nil
|
||||
}
|
||||
|
||||
// Member returns the baggage list-member identified by key.
|
||||
//
|
||||
// If there is no list-member matching the passed key the returned Member will
|
||||
// be a zero-value Member.
|
||||
// The returned member is not validated, as we assume the validation happened
|
||||
// when it was added to the Baggage.
|
||||
func (b Baggage) Member(key string) Member {
|
||||
v, ok := b.list[key]
|
||||
if !ok {
|
||||
// We do not need to worry about distinguishing between the situation
|
||||
// where a zero-valued Member is included in the Baggage because a
|
||||
// zero-valued Member is invalid according to the W3C Baggage
|
||||
// specification (it has an empty key).
|
||||
return newInvalidMember()
|
||||
}
|
||||
|
||||
return Member{
|
||||
key: key,
|
||||
value: v.Value,
|
||||
properties: fromInternalProperties(v.Properties),
|
||||
hasData: true,
|
||||
}
|
||||
}
|
||||
|
||||
// Members returns all the baggage list-members.
|
||||
// The order of the returned list-members does not have significance.
|
||||
//
|
||||
// The returned members are not validated, as we assume the validation happened
|
||||
// when they were added to the Baggage.
|
||||
func (b Baggage) Members() []Member {
|
||||
if len(b.list) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
members := make([]Member, 0, len(b.list))
|
||||
for k, v := range b.list {
|
||||
members = append(members, Member{
|
||||
key: k,
|
||||
value: v.Value,
|
||||
properties: fromInternalProperties(v.Properties),
|
||||
hasData: true,
|
||||
})
|
||||
}
|
||||
return members
|
||||
}
|
||||
|
||||
// SetMember returns a copy the Baggage with the member included. If the
|
||||
// baggage contains a Member with the same key the existing Member is
|
||||
// replaced.
|
||||
//
|
||||
// If member is invalid according to the W3C Baggage specification, an error
|
||||
// is returned with the original Baggage.
|
||||
func (b Baggage) SetMember(member Member) (Baggage, error) {
|
||||
if !member.hasData {
|
||||
return b, errInvalidMember
|
||||
}
|
||||
|
||||
n := len(b.list)
|
||||
if _, ok := b.list[member.key]; !ok {
|
||||
n++
|
||||
}
|
||||
list := make(baggage.List, n)
|
||||
|
||||
for k, v := range b.list {
|
||||
// Do not copy if we are just going to overwrite.
|
||||
if k == member.key {
|
||||
continue
|
||||
}
|
||||
list[k] = v
|
||||
}
|
||||
|
||||
list[member.key] = baggage.Item{
|
||||
Value: member.value,
|
||||
Properties: member.properties.asInternal(),
|
||||
}
|
||||
|
||||
return Baggage{list: list}, nil
|
||||
}
|
||||
|
||||
// DeleteMember returns a copy of the Baggage with the list-member identified
|
||||
// by key removed.
|
||||
func (b Baggage) DeleteMember(key string) Baggage {
|
||||
n := len(b.list)
|
||||
if _, ok := b.list[key]; ok {
|
||||
n--
|
||||
}
|
||||
list := make(baggage.List, n)
|
||||
|
||||
for k, v := range b.list {
|
||||
if k == key {
|
||||
continue
|
||||
}
|
||||
list[k] = v
|
||||
}
|
||||
|
||||
return Baggage{list: list}
|
||||
}
|
||||
|
||||
// Len returns the number of list-members in the Baggage.
|
||||
func (b Baggage) Len() int {
|
||||
return len(b.list)
|
||||
}
|
||||
|
||||
// String encodes Baggage into a string compliant with the W3C Baggage
|
||||
// specification. The returned string will be invalid if the Baggage contains
|
||||
// any invalid list-members.
|
||||
func (b Baggage) String() string {
|
||||
members := make([]string, 0, len(b.list))
|
||||
for k, v := range b.list {
|
||||
members = append(members, Member{
|
||||
key: k,
|
||||
value: v.Value,
|
||||
properties: fromInternalProperties(v.Properties),
|
||||
}.String())
|
||||
}
|
||||
return strings.Join(members, listDelimiter)
|
||||
}
|
||||
45
vendor/github.com/getsentry/sentry-go/internal/otel/baggage/internal/baggage/baggage.go
generated
vendored
Normal file
45
vendor/github.com/getsentry/sentry-go/internal/otel/baggage/internal/baggage/baggage.go
generated
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
// Adapted from https://github.com/open-telemetry/opentelemetry-go/blob/c21b6b6bb31a2f74edd06e262f1690f3f6ea3d5c/internal/baggage/baggage.go
|
||||
//
|
||||
// Copyright The OpenTelemetry Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
/*
|
||||
Package baggage provides base types and functionality to store and retrieve
|
||||
baggage in Go context. This package exists because the OpenTracing bridge to
|
||||
OpenTelemetry needs to synchronize state whenever baggage for a context is
|
||||
modified and that context contains an OpenTracing span. If it were not for
|
||||
this need this package would not need to exist and the
|
||||
`go.opentelemetry.io/otel/baggage` package would be the singular place where
|
||||
W3C baggage is handled.
|
||||
*/
|
||||
package baggage
|
||||
|
||||
// List is the collection of baggage members. The W3C allows for duplicates,
|
||||
// but OpenTelemetry does not, therefore, this is represented as a map.
|
||||
type List map[string]Item
|
||||
|
||||
// Item is the value and metadata properties part of a list-member.
|
||||
type Item struct {
|
||||
Value string
|
||||
Properties []Property
|
||||
}
|
||||
|
||||
// Property is a metadata entry for a list-member.
|
||||
type Property struct {
|
||||
Key, Value string
|
||||
|
||||
// HasValue indicates if a zero-value value means the property does not
|
||||
// have a value or if it was the zero-value.
|
||||
HasValue bool
|
||||
}
|
||||
45
vendor/github.com/getsentry/sentry-go/internal/ratelimit/category.go
generated
vendored
Normal file
45
vendor/github.com/getsentry/sentry-go/internal/ratelimit/category.go
generated
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
package ratelimit
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
// Reference:
|
||||
// https://github.com/getsentry/relay/blob/0424a2e017d193a93918053c90cdae9472d164bf/relay-common/src/constants.rs#L116-L127
|
||||
|
||||
// Category classifies supported payload types that can be ingested by Sentry
|
||||
// and, therefore, rate limited.
|
||||
type Category string
|
||||
|
||||
// Known rate limit categories. As a special case, the CategoryAll applies to
|
||||
// all known payload types.
|
||||
const (
|
||||
CategoryAll Category = ""
|
||||
CategoryError Category = "error"
|
||||
CategoryTransaction Category = "transaction"
|
||||
)
|
||||
|
||||
// knownCategories is the set of currently known categories. Other categories
|
||||
// are ignored for the purpose of rate-limiting.
|
||||
var knownCategories = map[Category]struct{}{
|
||||
CategoryAll: {},
|
||||
CategoryError: {},
|
||||
CategoryTransaction: {},
|
||||
}
|
||||
|
||||
// String returns the category formatted for debugging.
|
||||
func (c Category) String() string {
|
||||
if c == "" {
|
||||
return "CategoryAll"
|
||||
}
|
||||
|
||||
caser := cases.Title(language.English)
|
||||
rv := "Category"
|
||||
for _, w := range strings.Fields(string(c)) {
|
||||
rv += caser.String(w)
|
||||
}
|
||||
return rv
|
||||
}
|
||||
22
vendor/github.com/getsentry/sentry-go/internal/ratelimit/deadline.go
generated
vendored
Normal file
22
vendor/github.com/getsentry/sentry-go/internal/ratelimit/deadline.go
generated
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
package ratelimit
|
||||
|
||||
import "time"
|
||||
|
||||
// A Deadline is a time instant when a rate limit expires.
|
||||
type Deadline time.Time
|
||||
|
||||
// After reports whether the deadline d is after other.
|
||||
func (d Deadline) After(other Deadline) bool {
|
||||
return time.Time(d).After(time.Time(other))
|
||||
}
|
||||
|
||||
// Equal reports whether d and e represent the same deadline.
|
||||
func (d Deadline) Equal(e Deadline) bool {
|
||||
return time.Time(d).Equal(time.Time(e))
|
||||
}
|
||||
|
||||
// String returns the deadline formatted for debugging.
|
||||
func (d Deadline) String() string {
|
||||
// Like time.Time.String, but without the monotonic clock reading.
|
||||
return time.Time(d).Round(0).String()
|
||||
}
|
||||
3
vendor/github.com/getsentry/sentry-go/internal/ratelimit/doc.go
generated
vendored
Normal file
3
vendor/github.com/getsentry/sentry-go/internal/ratelimit/doc.go
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
// Package ratelimit provides tools to work with rate limits imposed by Sentry's
|
||||
// data ingestion pipeline.
|
||||
package ratelimit
|
||||
64
vendor/github.com/getsentry/sentry-go/internal/ratelimit/map.go
generated
vendored
Normal file
64
vendor/github.com/getsentry/sentry-go/internal/ratelimit/map.go
generated
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
package ratelimit
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Map maps categories to rate limit deadlines.
|
||||
//
|
||||
// A rate limit is in effect for a given category if either the category's
|
||||
// deadline or the deadline for the special CategoryAll has not yet expired.
|
||||
//
|
||||
// Use IsRateLimited to check whether a category is rate-limited.
|
||||
type Map map[Category]Deadline
|
||||
|
||||
// IsRateLimited returns true if the category is currently rate limited.
|
||||
func (m Map) IsRateLimited(c Category) bool {
|
||||
return m.isRateLimited(c, time.Now())
|
||||
}
|
||||
|
||||
func (m Map) isRateLimited(c Category, now time.Time) bool {
|
||||
return m.Deadline(c).After(Deadline(now))
|
||||
}
|
||||
|
||||
// Deadline returns the deadline when the rate limit for the given category or
|
||||
// the special CategoryAll expire, whichever is furthest into the future.
|
||||
func (m Map) Deadline(c Category) Deadline {
|
||||
categoryDeadline := m[c]
|
||||
allDeadline := m[CategoryAll]
|
||||
if categoryDeadline.After(allDeadline) {
|
||||
return categoryDeadline
|
||||
}
|
||||
return allDeadline
|
||||
}
|
||||
|
||||
// Merge merges the other map into m.
|
||||
//
|
||||
// If a category appears in both maps, the deadline that is furthest into the
|
||||
// future is preserved.
|
||||
func (m Map) Merge(other Map) {
|
||||
for c, d := range other {
|
||||
if d.After(m[c]) {
|
||||
m[c] = d
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FromResponse returns a rate limit map from an HTTP response.
|
||||
func FromResponse(r *http.Response) Map {
|
||||
return fromResponse(r, time.Now())
|
||||
}
|
||||
|
||||
func fromResponse(r *http.Response, now time.Time) Map {
|
||||
s := r.Header.Get("X-Sentry-Rate-Limits")
|
||||
if s != "" {
|
||||
return parseXSentryRateLimits(s, now)
|
||||
}
|
||||
if r.StatusCode == http.StatusTooManyRequests {
|
||||
s := r.Header.Get("Retry-After")
|
||||
deadline, _ := parseRetryAfter(s, now)
|
||||
return Map{CategoryAll: deadline}
|
||||
}
|
||||
return Map{}
|
||||
}
|
||||
76
vendor/github.com/getsentry/sentry-go/internal/ratelimit/rate_limits.go
generated
vendored
Normal file
76
vendor/github.com/getsentry/sentry-go/internal/ratelimit/rate_limits.go
generated
vendored
Normal file
@@ -0,0 +1,76 @@
|
||||
package ratelimit
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var errInvalidXSRLRetryAfter = errors.New("invalid retry-after value")
|
||||
|
||||
// parseXSentryRateLimits returns a RateLimits map by parsing an input string in
|
||||
// the format of the X-Sentry-Rate-Limits header.
|
||||
//
|
||||
// Example
|
||||
//
|
||||
// X-Sentry-Rate-Limits: 60:transaction, 2700:default;error;security
|
||||
//
|
||||
// This will rate limit transactions for the next 60 seconds and errors for the
|
||||
// next 2700 seconds.
|
||||
//
|
||||
// Limits for unknown categories are ignored.
|
||||
func parseXSentryRateLimits(s string, now time.Time) Map {
|
||||
// https://github.com/getsentry/relay/blob/0424a2e017d193a93918053c90cdae9472d164bf/relay-server/src/utils/rate_limits.rs#L44-L82
|
||||
m := make(Map, len(knownCategories))
|
||||
for _, limit := range strings.Split(s, ",") {
|
||||
limit = strings.TrimSpace(limit)
|
||||
if limit == "" {
|
||||
continue
|
||||
}
|
||||
components := strings.Split(limit, ":")
|
||||
if len(components) == 0 {
|
||||
continue
|
||||
}
|
||||
retryAfter, err := parseXSRLRetryAfter(strings.TrimSpace(components[0]), now)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
categories := ""
|
||||
if len(components) > 1 {
|
||||
categories = components[1]
|
||||
}
|
||||
for _, category := range strings.Split(categories, ";") {
|
||||
c := Category(strings.ToLower(strings.TrimSpace(category)))
|
||||
if _, ok := knownCategories[c]; !ok {
|
||||
// skip unknown categories, keep m small
|
||||
continue
|
||||
}
|
||||
// always keep the deadline furthest into the future
|
||||
if retryAfter.After(m[c]) {
|
||||
m[c] = retryAfter
|
||||
}
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// parseXSRLRetryAfter parses a string into a retry-after rate limit deadline.
|
||||
//
|
||||
// Valid input is a number, possibly signed and possibly floating-point,
|
||||
// indicating the number of seconds to wait before sending another request.
|
||||
// Negative values are treated as zero. Fractional values are rounded to the
|
||||
// next integer.
|
||||
func parseXSRLRetryAfter(s string, now time.Time) (Deadline, error) {
|
||||
// https://github.com/getsentry/relay/blob/0424a2e017d193a93918053c90cdae9472d164bf/relay-quotas/src/rate_limit.rs#L88-L96
|
||||
f, err := strconv.ParseFloat(s, 64)
|
||||
if err != nil {
|
||||
return Deadline{}, errInvalidXSRLRetryAfter
|
||||
}
|
||||
d := time.Duration(math.Ceil(math.Max(f, 0.0))) * time.Second
|
||||
if d < 0 {
|
||||
d = 0
|
||||
}
|
||||
return Deadline(now.Add(d)), nil
|
||||
}
|
||||
40
vendor/github.com/getsentry/sentry-go/internal/ratelimit/retry_after.go
generated
vendored
Normal file
40
vendor/github.com/getsentry/sentry-go/internal/ratelimit/retry_after.go
generated
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
package ratelimit
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
const defaultRetryAfter = 1 * time.Minute
|
||||
|
||||
var errInvalidRetryAfter = errors.New("invalid input")
|
||||
|
||||
// parseRetryAfter parses a string s as in the standard Retry-After HTTP header
|
||||
// and returns a deadline until when requests are rate limited and therefore new
|
||||
// requests should not be sent. The input may be either a date or a non-negative
|
||||
// integer number of seconds.
|
||||
//
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
|
||||
//
|
||||
// parseRetryAfter always returns a usable deadline, even in case of an error.
|
||||
//
|
||||
// This is the original rate limiting mechanism used by Sentry, superseeded by
|
||||
// the X-Sentry-Rate-Limits response header.
|
||||
func parseRetryAfter(s string, now time.Time) (Deadline, error) {
|
||||
if s == "" {
|
||||
goto invalid
|
||||
}
|
||||
if n, err := strconv.Atoi(s); err == nil {
|
||||
if n < 0 {
|
||||
goto invalid
|
||||
}
|
||||
d := time.Duration(n) * time.Second
|
||||
return Deadline(now.Add(d)), nil
|
||||
}
|
||||
if date, err := time.Parse(time.RFC1123, s); err == nil {
|
||||
return Deadline(date), nil
|
||||
}
|
||||
invalid:
|
||||
return Deadline(now.Add(defaultRetryAfter)), errInvalidRetryAfter
|
||||
}
|
||||
216
vendor/github.com/getsentry/sentry-go/log.go
generated
vendored
Normal file
216
vendor/github.com/getsentry/sentry-go/log.go
generated
vendored
Normal file
@@ -0,0 +1,216 @@
|
||||
package sentry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/getsentry/sentry-go/attribute"
|
||||
)
|
||||
|
||||
type LogLevel string
|
||||
|
||||
const (
|
||||
LogLevelTrace Level = "trace"
|
||||
LogLevelDebug Level = "debug"
|
||||
LogLevelInfo Level = "info"
|
||||
LogLevelWarn Level = "warn"
|
||||
LogLevelError Level = "error"
|
||||
LogLevelFatal Level = "fatal"
|
||||
)
|
||||
|
||||
const (
|
||||
LogSeverityTrace int = 1
|
||||
LogSeverityDebug int = 5
|
||||
LogSeverityInfo int = 9
|
||||
LogSeverityWarning int = 13
|
||||
LogSeverityError int = 17
|
||||
LogSeverityFatal int = 21
|
||||
)
|
||||
|
||||
var mapTypesToStr = map[attribute.Type]string{
|
||||
attribute.INVALID: "",
|
||||
attribute.BOOL: "boolean",
|
||||
attribute.INT64: "integer",
|
||||
attribute.FLOAT64: "double",
|
||||
attribute.STRING: "string",
|
||||
}
|
||||
|
||||
type sentryLogger struct {
|
||||
client *Client
|
||||
attributes map[string]Attribute
|
||||
}
|
||||
|
||||
// NewLogger returns a Logger that emits logs to Sentry. If logging is turned off, all logs get discarded.
|
||||
func NewLogger(ctx context.Context) Logger {
|
||||
var hub *Hub
|
||||
hub = GetHubFromContext(ctx)
|
||||
if hub == nil {
|
||||
hub = CurrentHub()
|
||||
}
|
||||
|
||||
client := hub.Client()
|
||||
if client != nil && client.batchLogger != nil {
|
||||
return &sentryLogger{client, make(map[string]Attribute)}
|
||||
}
|
||||
|
||||
DebugLogger.Println("fallback to noopLogger: enableLogs disabled")
|
||||
return &noopLogger{} // fallback: does nothing
|
||||
}
|
||||
|
||||
func (l *sentryLogger) Write(p []byte) (int, error) {
|
||||
// Avoid sending double newlines to Sentry
|
||||
msg := strings.TrimRight(string(p), "\n")
|
||||
l.log(context.Background(), LevelInfo, LogSeverityInfo, msg)
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (l *sentryLogger) log(ctx context.Context, level Level, severity int, message string, args ...interface{}) {
|
||||
if message == "" {
|
||||
return
|
||||
}
|
||||
hub := GetHubFromContext(ctx)
|
||||
if hub == nil {
|
||||
hub = CurrentHub()
|
||||
}
|
||||
|
||||
var traceID TraceID
|
||||
var spanID SpanID
|
||||
|
||||
span := hub.Scope().span
|
||||
if span != nil {
|
||||
traceID = span.TraceID
|
||||
spanID = span.SpanID
|
||||
} else {
|
||||
traceID = hub.Scope().propagationContext.TraceID
|
||||
}
|
||||
|
||||
attrs := map[string]Attribute{}
|
||||
if len(args) > 0 {
|
||||
attrs["sentry.message.template"] = Attribute{
|
||||
Value: message, Type: "string",
|
||||
}
|
||||
for i, p := range args {
|
||||
attrs[fmt.Sprintf("sentry.message.parameters.%d", i)] = Attribute{
|
||||
Value: fmt.Sprint(p), Type: "string",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If `log` was called with SetAttributes, pass the attributes to attrs
|
||||
if len(l.attributes) > 0 {
|
||||
for k, v := range l.attributes {
|
||||
attrs[k] = v
|
||||
}
|
||||
// flush attributes from logger after send
|
||||
clear(l.attributes)
|
||||
}
|
||||
|
||||
// Set default attributes
|
||||
if release := l.client.options.Release; release != "" {
|
||||
attrs["sentry.release"] = Attribute{Value: release, Type: "string"}
|
||||
}
|
||||
if environment := l.client.options.Environment; environment != "" {
|
||||
attrs["sentry.environment"] = Attribute{Value: environment, Type: "string"}
|
||||
}
|
||||
if serverName := l.client.options.ServerName; serverName != "" {
|
||||
attrs["sentry.server.address"] = Attribute{Value: serverName, Type: "string"}
|
||||
} else if serverAddr, err := os.Hostname(); err == nil {
|
||||
attrs["sentry.server.address"] = Attribute{Value: serverAddr, Type: "string"}
|
||||
}
|
||||
if spanID.String() != "0000000000000000" {
|
||||
attrs["sentry.trace.parent_span_id"] = Attribute{Value: spanID.String(), Type: "string"}
|
||||
}
|
||||
if sdkIdentifier := l.client.sdkIdentifier; sdkIdentifier != "" {
|
||||
attrs["sentry.sdk.name"] = Attribute{Value: sdkIdentifier, Type: "string"}
|
||||
}
|
||||
if sdkVersion := l.client.sdkVersion; sdkVersion != "" {
|
||||
attrs["sentry.sdk.version"] = Attribute{Value: sdkVersion, Type: "string"}
|
||||
}
|
||||
attrs["sentry.origin"] = Attribute{Value: "auto.logger.log", Type: "string"}
|
||||
|
||||
log := &Log{
|
||||
Timestamp: time.Now(),
|
||||
TraceID: traceID,
|
||||
Level: level,
|
||||
Severity: severity,
|
||||
Body: fmt.Sprintf(message, args...),
|
||||
Attributes: attrs,
|
||||
}
|
||||
|
||||
if l.client.options.BeforeSendLog != nil {
|
||||
log = l.client.options.BeforeSendLog(log)
|
||||
}
|
||||
|
||||
if log != nil {
|
||||
l.client.batchLogger.logCh <- *log
|
||||
}
|
||||
|
||||
if l.client.options.Debug {
|
||||
DebugLogger.Printf(message, args...)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *sentryLogger) SetAttributes(attrs ...attribute.Builder) {
|
||||
for _, v := range attrs {
|
||||
t, ok := mapTypesToStr[v.Value.Type()]
|
||||
if !ok || t == "" {
|
||||
DebugLogger.Printf("invalid attribute type set: %v", t)
|
||||
continue
|
||||
}
|
||||
|
||||
l.attributes[v.Key] = Attribute{
|
||||
Value: v.Value.AsInterface(),
|
||||
Type: t,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l *sentryLogger) Trace(ctx context.Context, v ...interface{}) {
|
||||
l.log(ctx, LogLevelTrace, LogSeverityTrace, fmt.Sprint(v...))
|
||||
}
|
||||
func (l *sentryLogger) Debug(ctx context.Context, v ...interface{}) {
|
||||
l.log(ctx, LogLevelDebug, LogSeverityDebug, fmt.Sprint(v...))
|
||||
}
|
||||
func (l *sentryLogger) Info(ctx context.Context, v ...interface{}) {
|
||||
l.log(ctx, LogLevelInfo, LogSeverityInfo, fmt.Sprint(v...))
|
||||
}
|
||||
func (l *sentryLogger) Warn(ctx context.Context, v ...interface{}) {
|
||||
l.log(ctx, LogLevelWarn, LogSeverityWarning, fmt.Sprint(v...))
|
||||
}
|
||||
func (l *sentryLogger) Error(ctx context.Context, v ...interface{}) {
|
||||
l.log(ctx, LogLevelError, LogSeverityError, fmt.Sprint(v...))
|
||||
}
|
||||
func (l *sentryLogger) Fatal(ctx context.Context, v ...interface{}) {
|
||||
l.log(ctx, LogLevelFatal, LogSeverityFatal, fmt.Sprint(v...))
|
||||
os.Exit(1)
|
||||
}
|
||||
func (l *sentryLogger) Panic(ctx context.Context, v ...interface{}) {
|
||||
l.log(ctx, LogLevelFatal, LogSeverityFatal, fmt.Sprint(v...))
|
||||
panic(fmt.Sprint(v...))
|
||||
}
|
||||
func (l *sentryLogger) Tracef(ctx context.Context, format string, v ...interface{}) {
|
||||
l.log(ctx, LogLevelTrace, LogSeverityTrace, format, v...)
|
||||
}
|
||||
func (l *sentryLogger) Debugf(ctx context.Context, format string, v ...interface{}) {
|
||||
l.log(ctx, LogLevelDebug, LogSeverityDebug, format, v...)
|
||||
}
|
||||
func (l *sentryLogger) Infof(ctx context.Context, format string, v ...interface{}) {
|
||||
l.log(ctx, LogLevelInfo, LogSeverityInfo, format, v...)
|
||||
}
|
||||
func (l *sentryLogger) Warnf(ctx context.Context, format string, v ...interface{}) {
|
||||
l.log(ctx, LogLevelWarn, LogSeverityWarning, format, v...)
|
||||
}
|
||||
func (l *sentryLogger) Errorf(ctx context.Context, format string, v ...interface{}) {
|
||||
l.log(ctx, LogLevelError, LogSeverityError, format, v...)
|
||||
}
|
||||
func (l *sentryLogger) Fatalf(ctx context.Context, format string, v ...interface{}) {
|
||||
l.log(ctx, LogLevelFatal, LogSeverityFatal, format, v...)
|
||||
os.Exit(1)
|
||||
}
|
||||
func (l *sentryLogger) Panicf(ctx context.Context, format string, v ...interface{}) {
|
||||
l.log(ctx, LogLevelFatal, LogSeverityFatal, format, v...)
|
||||
panic(fmt.Sprint(v...))
|
||||
}
|
||||
65
vendor/github.com/getsentry/sentry-go/log_fallback.go
generated
vendored
Normal file
65
vendor/github.com/getsentry/sentry-go/log_fallback.go
generated
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
package sentry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/getsentry/sentry-go/attribute"
|
||||
)
|
||||
|
||||
// Fallback, no-op logger if logging is disabled.
|
||||
type noopLogger struct{}
|
||||
|
||||
func (*noopLogger) Trace(_ context.Context, _ ...interface{}) {
|
||||
DebugLogger.Printf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", LogLevelTrace)
|
||||
}
|
||||
func (*noopLogger) Debug(_ context.Context, _ ...interface{}) {
|
||||
DebugLogger.Printf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", LogLevelDebug)
|
||||
}
|
||||
func (*noopLogger) Info(_ context.Context, _ ...interface{}) {
|
||||
DebugLogger.Printf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", LogLevelInfo)
|
||||
}
|
||||
func (*noopLogger) Warn(_ context.Context, _ ...interface{}) {
|
||||
DebugLogger.Printf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", LogLevelWarn)
|
||||
}
|
||||
func (*noopLogger) Error(_ context.Context, _ ...interface{}) {
|
||||
DebugLogger.Printf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", LogLevelError)
|
||||
}
|
||||
func (*noopLogger) Fatal(_ context.Context, _ ...interface{}) {
|
||||
DebugLogger.Printf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", LogLevelFatal)
|
||||
os.Exit(1)
|
||||
}
|
||||
func (*noopLogger) Panic(_ context.Context, _ ...interface{}) {
|
||||
DebugLogger.Printf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", LogLevelFatal)
|
||||
panic(fmt.Sprintf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", LogLevelFatal))
|
||||
}
|
||||
func (*noopLogger) Tracef(_ context.Context, _ string, _ ...interface{}) {
|
||||
DebugLogger.Printf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", LogLevelTrace)
|
||||
}
|
||||
func (*noopLogger) Debugf(_ context.Context, _ string, _ ...interface{}) {
|
||||
DebugLogger.Printf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", LogLevelDebug)
|
||||
}
|
||||
func (*noopLogger) Infof(_ context.Context, _ string, _ ...interface{}) {
|
||||
DebugLogger.Printf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", LogLevelInfo)
|
||||
}
|
||||
func (*noopLogger) Warnf(_ context.Context, _ string, _ ...interface{}) {
|
||||
DebugLogger.Printf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", LogLevelWarn)
|
||||
}
|
||||
func (*noopLogger) Errorf(_ context.Context, _ string, _ ...interface{}) {
|
||||
DebugLogger.Printf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", LogLevelError)
|
||||
}
|
||||
func (*noopLogger) Fatalf(_ context.Context, _ string, _ ...interface{}) {
|
||||
DebugLogger.Printf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", LogLevelFatal)
|
||||
os.Exit(1)
|
||||
}
|
||||
func (*noopLogger) Panicf(_ context.Context, _ string, _ ...interface{}) {
|
||||
DebugLogger.Printf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", LogLevelFatal)
|
||||
panic(fmt.Sprintf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", LogLevelFatal))
|
||||
}
|
||||
func (*noopLogger) SetAttributes(...attribute.Builder) {
|
||||
DebugLogger.Printf("No attributes attached. Turn on logging via EnableLogs")
|
||||
}
|
||||
func (*noopLogger) Write(_ []byte) (n int, err error) {
|
||||
return 0, fmt.Errorf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", LogLevelInfo)
|
||||
}
|
||||
47
vendor/github.com/getsentry/sentry-go/mocks.go
generated
vendored
Normal file
47
vendor/github.com/getsentry/sentry-go/mocks.go
generated
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
package sentry
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// MockScope implements [Scope] for use in tests.
|
||||
type MockScope struct {
|
||||
breadcrumb *Breadcrumb
|
||||
shouldDropEvent bool
|
||||
}
|
||||
|
||||
func (scope *MockScope) AddBreadcrumb(breadcrumb *Breadcrumb, _ int) {
|
||||
scope.breadcrumb = breadcrumb
|
||||
}
|
||||
|
||||
func (scope *MockScope) ApplyToEvent(event *Event, _ *EventHint, _ *Client) *Event {
|
||||
if scope.shouldDropEvent {
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
}
|
||||
|
||||
// MockTransport implements [Transport] for use in tests.
|
||||
type MockTransport struct {
|
||||
mu sync.Mutex
|
||||
events []*Event
|
||||
lastEvent *Event
|
||||
}
|
||||
|
||||
func (t *MockTransport) Configure(_ ClientOptions) {}
|
||||
func (t *MockTransport) SendEvent(event *Event) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
t.events = append(t.events, event)
|
||||
t.lastEvent = event
|
||||
}
|
||||
func (t *MockTransport) Flush(_ time.Duration) bool {
|
||||
return true
|
||||
}
|
||||
func (t *MockTransport) Events() []*Event {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
return t.events
|
||||
}
|
||||
func (t *MockTransport) Close() {}
|
||||
90
vendor/github.com/getsentry/sentry-go/propagation_context.go
generated
vendored
Normal file
90
vendor/github.com/getsentry/sentry-go/propagation_context.go
generated
vendored
Normal 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
|
||||
}
|
||||
491
vendor/github.com/getsentry/sentry-go/scope.go
generated
vendored
Normal file
491
vendor/github.com/getsentry/sentry-go/scope.go
generated
vendored
Normal file
@@ -0,0 +1,491 @@
|
||||
package sentry
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Scope holds contextual data for the current scope.
|
||||
//
|
||||
// The scope is an object that can cloned efficiently and stores data that is
|
||||
// locally relevant to an event. For instance the scope will hold recorded
|
||||
// breadcrumbs and similar information.
|
||||
//
|
||||
// The scope can be interacted with in two ways. First, the scope is routinely
|
||||
// updated with information by functions such as AddBreadcrumb which will modify
|
||||
// the current scope. Second, the current scope can be configured through the
|
||||
// ConfigureScope function or Hub method of the same name.
|
||||
//
|
||||
// The scope is meant to be modified but not inspected directly. When preparing
|
||||
// an event for reporting, the current client adds information from the current
|
||||
// scope into the event.
|
||||
type Scope struct {
|
||||
mu sync.RWMutex
|
||||
breadcrumbs []*Breadcrumb
|
||||
attachments []*Attachment
|
||||
user User
|
||||
tags map[string]string
|
||||
contexts map[string]Context
|
||||
extra map[string]interface{}
|
||||
fingerprint []string
|
||||
level Level
|
||||
request *http.Request
|
||||
// requestBody holds a reference to the original request.Body.
|
||||
requestBody interface {
|
||||
// Bytes returns bytes from the original body, lazily buffered as the
|
||||
// original body is read.
|
||||
Bytes() []byte
|
||||
// Overflow returns true if the body is larger than the maximum buffer
|
||||
// size.
|
||||
Overflow() bool
|
||||
}
|
||||
eventProcessors []EventProcessor
|
||||
|
||||
propagationContext PropagationContext
|
||||
span *Span
|
||||
}
|
||||
|
||||
// NewScope creates a new Scope.
|
||||
func NewScope() *Scope {
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
||||
// AddBreadcrumb adds new breadcrumb to the current scope
|
||||
// and optionally throws the old one if limit is reached.
|
||||
func (scope *Scope) AddBreadcrumb(breadcrumb *Breadcrumb, limit int) {
|
||||
if breadcrumb.Timestamp.IsZero() {
|
||||
breadcrumb.Timestamp = time.Now()
|
||||
}
|
||||
|
||||
scope.mu.Lock()
|
||||
defer scope.mu.Unlock()
|
||||
|
||||
scope.breadcrumbs = append(scope.breadcrumbs, breadcrumb)
|
||||
if len(scope.breadcrumbs) > limit {
|
||||
scope.breadcrumbs = scope.breadcrumbs[1 : limit+1]
|
||||
}
|
||||
}
|
||||
|
||||
// ClearBreadcrumbs clears all breadcrumbs from the current scope.
|
||||
func (scope *Scope) ClearBreadcrumbs() {
|
||||
scope.mu.Lock()
|
||||
defer scope.mu.Unlock()
|
||||
|
||||
scope.breadcrumbs = []*Breadcrumb{}
|
||||
}
|
||||
|
||||
// AddAttachment adds new attachment to the current scope.
|
||||
func (scope *Scope) AddAttachment(attachment *Attachment) {
|
||||
scope.mu.Lock()
|
||||
defer scope.mu.Unlock()
|
||||
|
||||
scope.attachments = append(scope.attachments, attachment)
|
||||
}
|
||||
|
||||
// ClearAttachments clears all attachments from the current scope.
|
||||
func (scope *Scope) ClearAttachments() {
|
||||
scope.mu.Lock()
|
||||
defer scope.mu.Unlock()
|
||||
|
||||
scope.attachments = []*Attachment{}
|
||||
}
|
||||
|
||||
// SetUser sets the user for the current scope.
|
||||
func (scope *Scope) SetUser(user User) {
|
||||
scope.mu.Lock()
|
||||
defer scope.mu.Unlock()
|
||||
|
||||
scope.user = user
|
||||
}
|
||||
|
||||
// SetRequest sets the request for the current scope.
|
||||
func (scope *Scope) SetRequest(r *http.Request) {
|
||||
scope.mu.Lock()
|
||||
defer scope.mu.Unlock()
|
||||
|
||||
scope.request = r
|
||||
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Don't buffer request body if we know it is oversized.
|
||||
if r.ContentLength > maxRequestBodyBytes {
|
||||
return
|
||||
}
|
||||
// Don't buffer if there is no body.
|
||||
if r.Body == nil || r.Body == http.NoBody {
|
||||
return
|
||||
}
|
||||
buf := &limitedBuffer{Capacity: maxRequestBodyBytes}
|
||||
r.Body = readCloser{
|
||||
Reader: io.TeeReader(r.Body, buf),
|
||||
Closer: r.Body,
|
||||
}
|
||||
scope.requestBody = buf
|
||||
}
|
||||
|
||||
// SetRequestBody sets the request body for the current scope.
|
||||
//
|
||||
// This method should only be called when the body bytes are already available
|
||||
// in memory. Typically, the request body is buffered lazily from the
|
||||
// Request.Body from SetRequest.
|
||||
func (scope *Scope) SetRequestBody(b []byte) {
|
||||
scope.mu.Lock()
|
||||
defer scope.mu.Unlock()
|
||||
|
||||
capacity := maxRequestBodyBytes
|
||||
overflow := false
|
||||
if len(b) > capacity {
|
||||
overflow = true
|
||||
b = b[:capacity]
|
||||
}
|
||||
scope.requestBody = &limitedBuffer{
|
||||
Capacity: capacity,
|
||||
Buffer: *bytes.NewBuffer(b),
|
||||
overflow: overflow,
|
||||
}
|
||||
}
|
||||
|
||||
// maxRequestBodyBytes is the default maximum request body size to send to
|
||||
// Sentry.
|
||||
const maxRequestBodyBytes = 10 * 1024
|
||||
|
||||
// A limitedBuffer is like a bytes.Buffer, but limited to store at most Capacity
|
||||
// bytes. Any writes past the capacity are silently discarded, similar to
|
||||
// io.Discard.
|
||||
type limitedBuffer struct {
|
||||
Capacity int
|
||||
|
||||
bytes.Buffer
|
||||
overflow bool
|
||||
}
|
||||
|
||||
// Write implements io.Writer.
|
||||
func (b *limitedBuffer) Write(p []byte) (n int, err error) {
|
||||
// Silently ignore writes after overflow.
|
||||
if b.overflow {
|
||||
return len(p), nil
|
||||
}
|
||||
left := b.Capacity - b.Len()
|
||||
if left < 0 {
|
||||
left = 0
|
||||
}
|
||||
if len(p) > left {
|
||||
b.overflow = true
|
||||
p = p[:left]
|
||||
}
|
||||
return b.Buffer.Write(p)
|
||||
}
|
||||
|
||||
// Overflow returns true if the limitedBuffer discarded bytes written to it.
|
||||
func (b *limitedBuffer) Overflow() bool {
|
||||
return b.overflow
|
||||
}
|
||||
|
||||
// readCloser combines an io.Reader and an io.Closer to implement io.ReadCloser.
|
||||
type readCloser struct {
|
||||
io.Reader
|
||||
io.Closer
|
||||
}
|
||||
|
||||
// SetTag adds a tag to the current scope.
|
||||
func (scope *Scope) SetTag(key, value string) {
|
||||
scope.mu.Lock()
|
||||
defer scope.mu.Unlock()
|
||||
|
||||
scope.tags[key] = value
|
||||
}
|
||||
|
||||
// SetTags assigns multiple tags to the current scope.
|
||||
func (scope *Scope) SetTags(tags map[string]string) {
|
||||
scope.mu.Lock()
|
||||
defer scope.mu.Unlock()
|
||||
|
||||
for k, v := range tags {
|
||||
scope.tags[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveTag removes a tag from the current scope.
|
||||
func (scope *Scope) RemoveTag(key string) {
|
||||
scope.mu.Lock()
|
||||
defer scope.mu.Unlock()
|
||||
|
||||
delete(scope.tags, key)
|
||||
}
|
||||
|
||||
// SetContext adds a context to the current scope.
|
||||
func (scope *Scope) SetContext(key string, value Context) {
|
||||
scope.mu.Lock()
|
||||
defer scope.mu.Unlock()
|
||||
|
||||
scope.contexts[key] = value
|
||||
}
|
||||
|
||||
// SetContexts assigns multiple contexts to the current scope.
|
||||
func (scope *Scope) SetContexts(contexts map[string]Context) {
|
||||
scope.mu.Lock()
|
||||
defer scope.mu.Unlock()
|
||||
|
||||
for k, v := range contexts {
|
||||
scope.contexts[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveContext removes a context from the current scope.
|
||||
func (scope *Scope) RemoveContext(key string) {
|
||||
scope.mu.Lock()
|
||||
defer scope.mu.Unlock()
|
||||
|
||||
delete(scope.contexts, key)
|
||||
}
|
||||
|
||||
// SetExtra adds an extra to the current scope.
|
||||
func (scope *Scope) SetExtra(key string, value interface{}) {
|
||||
scope.mu.Lock()
|
||||
defer scope.mu.Unlock()
|
||||
|
||||
scope.extra[key] = value
|
||||
}
|
||||
|
||||
// SetExtras assigns multiple extras to the current scope.
|
||||
func (scope *Scope) SetExtras(extra map[string]interface{}) {
|
||||
scope.mu.Lock()
|
||||
defer scope.mu.Unlock()
|
||||
|
||||
for k, v := range extra {
|
||||
scope.extra[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveExtra removes a extra from the current scope.
|
||||
func (scope *Scope) RemoveExtra(key string) {
|
||||
scope.mu.Lock()
|
||||
defer scope.mu.Unlock()
|
||||
|
||||
delete(scope.extra, key)
|
||||
}
|
||||
|
||||
// SetFingerprint sets new fingerprint for the current scope.
|
||||
func (scope *Scope) SetFingerprint(fingerprint []string) {
|
||||
scope.mu.Lock()
|
||||
defer scope.mu.Unlock()
|
||||
|
||||
scope.fingerprint = fingerprint
|
||||
}
|
||||
|
||||
// SetLevel sets new level for the current scope.
|
||||
func (scope *Scope) SetLevel(level Level) {
|
||||
scope.mu.Lock()
|
||||
defer scope.mu.Unlock()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// GetSpan returns the span from the current scope.
|
||||
func (scope *Scope) GetSpan() *Span {
|
||||
return scope.span
|
||||
}
|
||||
|
||||
// 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()
|
||||
defer scope.mu.RUnlock()
|
||||
|
||||
clone := NewScope()
|
||||
clone.user = scope.user
|
||||
clone.breadcrumbs = make([]*Breadcrumb, len(scope.breadcrumbs))
|
||||
copy(clone.breadcrumbs, scope.breadcrumbs)
|
||||
clone.attachments = make([]*Attachment, len(scope.attachments))
|
||||
copy(clone.attachments, scope.attachments)
|
||||
for key, value := range scope.tags {
|
||||
clone.tags[key] = value
|
||||
}
|
||||
for key, value := range scope.contexts {
|
||||
clone.contexts[key] = cloneContext(value)
|
||||
}
|
||||
for key, value := range scope.extra {
|
||||
clone.extra[key] = value
|
||||
}
|
||||
clone.fingerprint = make([]string, len(scope.fingerprint))
|
||||
copy(clone.fingerprint, scope.fingerprint)
|
||||
clone.level = scope.level
|
||||
clone.request = scope.request
|
||||
clone.requestBody = scope.requestBody
|
||||
clone.eventProcessors = scope.eventProcessors
|
||||
clone.propagationContext = scope.propagationContext
|
||||
clone.span = scope.span
|
||||
return clone
|
||||
}
|
||||
|
||||
// Clear removes the data from the current scope. Not safe for concurrent use.
|
||||
func (scope *Scope) Clear() {
|
||||
*scope = *NewScope()
|
||||
}
|
||||
|
||||
// AddEventProcessor adds an event processor to the current scope.
|
||||
func (scope *Scope) AddEventProcessor(processor EventProcessor) {
|
||||
scope.mu.Lock()
|
||||
defer scope.mu.Unlock()
|
||||
|
||||
scope.eventProcessors = append(scope.eventProcessors, processor)
|
||||
}
|
||||
|
||||
// ApplyToEvent takes the data from the current scope and attaches it to the event.
|
||||
func (scope *Scope) ApplyToEvent(event *Event, hint *EventHint, client *Client) *Event {
|
||||
scope.mu.RLock()
|
||||
defer scope.mu.RUnlock()
|
||||
|
||||
if len(scope.breadcrumbs) > 0 {
|
||||
event.Breadcrumbs = append(event.Breadcrumbs, scope.breadcrumbs...)
|
||||
}
|
||||
|
||||
if len(scope.attachments) > 0 {
|
||||
event.Attachments = append(event.Attachments, scope.attachments...)
|
||||
}
|
||||
|
||||
if len(scope.tags) > 0 {
|
||||
if event.Tags == nil {
|
||||
event.Tags = make(map[string]string, len(scope.tags))
|
||||
}
|
||||
|
||||
for key, value := range scope.tags {
|
||||
event.Tags[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
if len(scope.contexts) > 0 {
|
||||
if event.Contexts == nil {
|
||||
event.Contexts = make(map[string]Context)
|
||||
}
|
||||
|
||||
for key, value := range scope.contexts {
|
||||
if key == "trace" && event.Type == transactionType {
|
||||
// Do not override trace context of
|
||||
// transactions, otherwise it breaks the
|
||||
// transaction event representation.
|
||||
// For error events, the trace context is used
|
||||
// to link errors and traces/spans in Sentry.
|
||||
continue
|
||||
}
|
||||
|
||||
// Ensure we are not overwriting event fields
|
||||
if _, ok := event.Contexts[key]; !ok {
|
||||
event.Contexts[key] = cloneContext(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
for key, value := range scope.extra {
|
||||
event.Extra[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
if event.User.IsEmpty() {
|
||||
event.User = scope.user
|
||||
}
|
||||
|
||||
if len(event.Fingerprint) == 0 {
|
||||
event.Fingerprint = append(event.Fingerprint, scope.fingerprint...)
|
||||
}
|
||||
|
||||
if scope.level != "" {
|
||||
event.Level = scope.level
|
||||
}
|
||||
|
||||
if event.Request == nil && scope.request != nil {
|
||||
event.Request = NewRequest(scope.request)
|
||||
// NOTE: The SDK does not attempt to send partial request body data.
|
||||
//
|
||||
// The reason being that Sentry's ingest pipeline and UI are optimized
|
||||
// to show structured data. Additionally, tooling around PII scrubbing
|
||||
// relies on structured data; truncated request bodies would create
|
||||
// invalid payloads that are more prone to leaking PII data.
|
||||
//
|
||||
// Users can still send more data along their events if they want to,
|
||||
// for example using Event.Extra.
|
||||
if scope.requestBody != nil && !scope.requestBody.Overflow() {
|
||||
event.Request.Data = string(scope.requestBody.Bytes())
|
||||
}
|
||||
}
|
||||
|
||||
for _, processor := range scope.eventProcessors {
|
||||
id := event.EventID
|
||||
event = processor(event, hint)
|
||||
if event == nil {
|
||||
DebugLogger.Printf("Event dropped by one of the Scope EventProcessors: %s\n", id)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return event
|
||||
}
|
||||
|
||||
// cloneContext returns a new context with keys and values copied from the passed one.
|
||||
//
|
||||
// Note: a new Context (map) is returned, but the function does NOT do
|
||||
// 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 := make(Context, len(c))
|
||||
for k, v := range c {
|
||||
res[k] = v
|
||||
}
|
||||
return res
|
||||
}
|
||||
132
vendor/github.com/getsentry/sentry-go/sentry.go
generated
vendored
Normal file
132
vendor/github.com/getsentry/sentry-go/sentry.go
generated
vendored
Normal file
@@ -0,0 +1,132 @@
|
||||
package sentry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
// The version of the SDK.
|
||||
const SDKVersion = "0.33.0"
|
||||
|
||||
// apiVersion is the minimum version of the Sentry API compatible with the
|
||||
// sentry-go SDK.
|
||||
const apiVersion = "7"
|
||||
|
||||
// Init initializes the SDK with options. The returned error is non-nil if
|
||||
// options is invalid, for instance if a malformed DSN is provided.
|
||||
func Init(options ClientOptions) error {
|
||||
hub := CurrentHub()
|
||||
client, err := NewClient(options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hub.BindClient(client)
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddBreadcrumb records a new breadcrumb.
|
||||
//
|
||||
// The total number of breadcrumbs that can be recorded are limited by the
|
||||
// configuration on the client.
|
||||
func AddBreadcrumb(breadcrumb *Breadcrumb) {
|
||||
hub := CurrentHub()
|
||||
hub.AddBreadcrumb(breadcrumb, nil)
|
||||
}
|
||||
|
||||
// CaptureMessage captures an arbitrary message.
|
||||
func CaptureMessage(message string) *EventID {
|
||||
hub := CurrentHub()
|
||||
return hub.CaptureMessage(message)
|
||||
}
|
||||
|
||||
// CaptureException captures an error.
|
||||
func CaptureException(exception error) *EventID {
|
||||
hub := CurrentHub()
|
||||
return hub.CaptureException(exception)
|
||||
}
|
||||
|
||||
// CaptureCheckIn captures a (cron) monitor check-in.
|
||||
func CaptureCheckIn(checkIn *CheckIn, monitorConfig *MonitorConfig) *EventID {
|
||||
hub := CurrentHub()
|
||||
return hub.CaptureCheckIn(checkIn, monitorConfig)
|
||||
}
|
||||
|
||||
// CaptureEvent captures an event on the currently active client if any.
|
||||
//
|
||||
// The event must already be assembled. Typically code would instead use
|
||||
// the utility methods like CaptureException. The return value is the
|
||||
// event ID. In case Sentry is disabled or event was dropped, the return value will be nil.
|
||||
func CaptureEvent(event *Event) *EventID {
|
||||
hub := CurrentHub()
|
||||
return hub.CaptureEvent(event)
|
||||
}
|
||||
|
||||
// Recover captures a panic.
|
||||
func Recover() *EventID {
|
||||
if err := recover(); err != nil {
|
||||
hub := CurrentHub()
|
||||
return hub.Recover(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RecoverWithContext captures a panic and passes relevant context object.
|
||||
func RecoverWithContext(ctx context.Context) *EventID {
|
||||
err := recover()
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
hub := GetHubFromContext(ctx)
|
||||
if hub == nil {
|
||||
hub = CurrentHub()
|
||||
}
|
||||
|
||||
return hub.RecoverWithContext(ctx, err)
|
||||
}
|
||||
|
||||
// WithScope is a shorthand for CurrentHub().WithScope.
|
||||
func WithScope(f func(scope *Scope)) {
|
||||
hub := CurrentHub()
|
||||
hub.WithScope(f)
|
||||
}
|
||||
|
||||
// ConfigureScope is a shorthand for CurrentHub().ConfigureScope.
|
||||
func ConfigureScope(f func(scope *Scope)) {
|
||||
hub := CurrentHub()
|
||||
hub.ConfigureScope(f)
|
||||
}
|
||||
|
||||
// PushScope is a shorthand for CurrentHub().PushScope.
|
||||
func PushScope() {
|
||||
hub := CurrentHub()
|
||||
hub.PushScope()
|
||||
}
|
||||
|
||||
// PopScope is a shorthand for CurrentHub().PopScope.
|
||||
func PopScope() {
|
||||
hub := CurrentHub()
|
||||
hub.PopScope()
|
||||
}
|
||||
|
||||
// Flush waits until the underlying Transport sends any buffered events 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 CaptureEvent,
|
||||
// CaptureException or CaptureMessage. Instead, to have the SDK send events over
|
||||
// the network synchronously, configure it to use the HTTPSyncTransport in the
|
||||
// call to Init.
|
||||
func Flush(timeout time.Duration) bool {
|
||||
hub := CurrentHub()
|
||||
return hub.Flush(timeout)
|
||||
}
|
||||
|
||||
// LastEventID returns an ID of last captured event.
|
||||
func LastEventID() EventID {
|
||||
hub := CurrentHub()
|
||||
return hub.LastEventID()
|
||||
}
|
||||
70
vendor/github.com/getsentry/sentry-go/sourcereader.go
generated
vendored
Normal file
70
vendor/github.com/getsentry/sentry-go/sourcereader.go
generated
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
package sentry
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type sourceReader struct {
|
||||
mu sync.Mutex
|
||||
cache map[string][][]byte
|
||||
}
|
||||
|
||||
func newSourceReader() sourceReader {
|
||||
return sourceReader{
|
||||
cache: make(map[string][][]byte),
|
||||
}
|
||||
}
|
||||
|
||||
func (sr *sourceReader) readContextLines(filename string, line, context int) ([][]byte, int) {
|
||||
sr.mu.Lock()
|
||||
defer sr.mu.Unlock()
|
||||
|
||||
lines, ok := sr.cache[filename]
|
||||
|
||||
if !ok {
|
||||
data, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
sr.cache[filename] = nil
|
||||
return nil, 0
|
||||
}
|
||||
lines = bytes.Split(data, []byte{'\n'})
|
||||
sr.cache[filename] = lines
|
||||
}
|
||||
|
||||
return sr.calculateContextLines(lines, line, context)
|
||||
}
|
||||
|
||||
func (sr *sourceReader) calculateContextLines(lines [][]byte, line, context int) ([][]byte, int) {
|
||||
// Stacktrace lines are 1-indexed, slices are 0-indexed
|
||||
line--
|
||||
|
||||
// contextLine points to a line that caused an issue itself, in relation to
|
||||
// returned slice.
|
||||
contextLine := context
|
||||
|
||||
if lines == nil || line >= len(lines) || line < 0 {
|
||||
return nil, 0
|
||||
}
|
||||
|
||||
if context < 0 {
|
||||
context = 0
|
||||
contextLine = 0
|
||||
}
|
||||
|
||||
start := line - context
|
||||
|
||||
if start < 0 {
|
||||
contextLine += start
|
||||
start = 0
|
||||
}
|
||||
|
||||
end := line + context + 1
|
||||
|
||||
if end > len(lines) {
|
||||
end = len(lines)
|
||||
}
|
||||
|
||||
return lines[start:end], contextLine
|
||||
}
|
||||
56
vendor/github.com/getsentry/sentry-go/span_recorder.go
generated
vendored
Normal file
56
vendor/github.com/getsentry/sentry-go/span_recorder.go
generated
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
package sentry
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// A spanRecorder stores a span tree that makes up a transaction. Safe for
|
||||
// concurrent use. It is okay to add child spans from multiple goroutines.
|
||||
type spanRecorder struct {
|
||||
mu sync.Mutex
|
||||
spans []*Span
|
||||
overflowOnce sync.Once
|
||||
}
|
||||
|
||||
// record stores a span. The first stored span is assumed to be the root of a
|
||||
// span tree.
|
||||
func (r *spanRecorder) record(s *Span) {
|
||||
maxSpans := defaultMaxSpans
|
||||
if client := CurrentHub().Client(); client != nil {
|
||||
maxSpans = client.options.MaxSpans
|
||||
}
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if len(r.spans) >= maxSpans {
|
||||
r.overflowOnce.Do(func() {
|
||||
root := r.spans[0]
|
||||
DebugLogger.Printf("Too many spans: dropping spans from transaction with TraceID=%s SpanID=%s limit=%d",
|
||||
root.TraceID, root.SpanID, maxSpans)
|
||||
})
|
||||
// TODO(tracing): mark the transaction event in some way to
|
||||
// communicate that spans were dropped.
|
||||
return
|
||||
}
|
||||
r.spans = append(r.spans, s)
|
||||
}
|
||||
|
||||
// root returns the first recorded span. Returns nil if none have been recorded.
|
||||
func (r *spanRecorder) root() *Span {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if len(r.spans) == 0 {
|
||||
return nil
|
||||
}
|
||||
return r.spans[0]
|
||||
}
|
||||
|
||||
// children returns a list of all recorded spans, except the root. Returns nil
|
||||
// if there are no children.
|
||||
func (r *spanRecorder) children() []*Span {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if len(r.spans) < 2 {
|
||||
return nil
|
||||
}
|
||||
return r.spans[1:]
|
||||
}
|
||||
407
vendor/github.com/getsentry/sentry-go/stacktrace.go
generated
vendored
Normal file
407
vendor/github.com/getsentry/sentry-go/stacktrace.go
generated
vendored
Normal file
@@ -0,0 +1,407 @@
|
||||
package sentry
|
||||
|
||||
import (
|
||||
"go/build"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const unknown string = "unknown"
|
||||
|
||||
// The module download is split into two parts: downloading the go.mod and downloading the actual code.
|
||||
// If you have dependencies only needed for tests, then they will show up in your go.mod,
|
||||
// and go get will download their go.mods, but it will not download their code.
|
||||
// The test-only dependencies get downloaded only when you need it, such as the first time you run go test.
|
||||
//
|
||||
// https://github.com/golang/go/issues/26913#issuecomment-411976222
|
||||
|
||||
// Stacktrace holds information about the frames of the stack.
|
||||
type Stacktrace struct {
|
||||
Frames []Frame `json:"frames,omitempty"`
|
||||
FramesOmitted []uint `json:"frames_omitted,omitempty"`
|
||||
}
|
||||
|
||||
// NewStacktrace creates a stacktrace using runtime.Callers.
|
||||
func NewStacktrace() *Stacktrace {
|
||||
pcs := make([]uintptr, 100)
|
||||
n := runtime.Callers(1, pcs)
|
||||
|
||||
if n == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
runtimeFrames := extractFrames(pcs[:n])
|
||||
frames := createFrames(runtimeFrames)
|
||||
|
||||
stacktrace := Stacktrace{
|
||||
Frames: frames,
|
||||
}
|
||||
|
||||
return &stacktrace
|
||||
}
|
||||
|
||||
// TODO: Make it configurable so that anyone can provide their own implementation?
|
||||
// Use of reflection allows us to not have a hard dependency on any given
|
||||
// package, so we don't have to import it.
|
||||
|
||||
// ExtractStacktrace creates a new Stacktrace based on the given error.
|
||||
func ExtractStacktrace(err error) *Stacktrace {
|
||||
method := extractReflectedStacktraceMethod(err)
|
||||
|
||||
var pcs []uintptr
|
||||
|
||||
if method.IsValid() {
|
||||
pcs = extractPcs(method)
|
||||
} else {
|
||||
pcs = extractXErrorsPC(err)
|
||||
}
|
||||
|
||||
if len(pcs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
runtimeFrames := extractFrames(pcs)
|
||||
frames := createFrames(runtimeFrames)
|
||||
|
||||
stacktrace := Stacktrace{
|
||||
Frames: frames,
|
||||
}
|
||||
|
||||
return &stacktrace
|
||||
}
|
||||
|
||||
func extractReflectedStacktraceMethod(err error) reflect.Value {
|
||||
errValue := reflect.ValueOf(err)
|
||||
|
||||
// https://github.com/go-errors/errors
|
||||
methodStackFrames := errValue.MethodByName("StackFrames")
|
||||
if methodStackFrames.IsValid() {
|
||||
return methodStackFrames
|
||||
}
|
||||
|
||||
// https://github.com/pkg/errors
|
||||
methodStackTrace := errValue.MethodByName("StackTrace")
|
||||
if methodStackTrace.IsValid() {
|
||||
return methodStackTrace
|
||||
}
|
||||
|
||||
// https://github.com/pingcap/errors
|
||||
methodGetStackTracer := errValue.MethodByName("GetStackTracer")
|
||||
if methodGetStackTracer.IsValid() {
|
||||
stacktracer := methodGetStackTracer.Call(nil)[0]
|
||||
stacktracerStackTrace := reflect.ValueOf(stacktracer).MethodByName("StackTrace")
|
||||
|
||||
if stacktracerStackTrace.IsValid() {
|
||||
return stacktracerStackTrace
|
||||
}
|
||||
}
|
||||
|
||||
return reflect.Value{}
|
||||
}
|
||||
|
||||
func extractPcs(method reflect.Value) []uintptr {
|
||||
var pcs []uintptr
|
||||
|
||||
stacktrace := method.Call(nil)[0]
|
||||
|
||||
if stacktrace.Kind() != reflect.Slice {
|
||||
return nil
|
||||
}
|
||||
|
||||
for i := 0; i < stacktrace.Len(); i++ {
|
||||
pc := stacktrace.Index(i)
|
||||
|
||||
switch pc.Kind() {
|
||||
case reflect.Uintptr:
|
||||
pcs = append(pcs, uintptr(pc.Uint()))
|
||||
case reflect.Struct:
|
||||
for _, fieldName := range []string{"ProgramCounter", "PC"} {
|
||||
field := pc.FieldByName(fieldName)
|
||||
if !field.IsValid() {
|
||||
continue
|
||||
}
|
||||
if field.Kind() == reflect.Uintptr {
|
||||
pcs = append(pcs, uintptr(field.Uint()))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return pcs
|
||||
}
|
||||
|
||||
// extractXErrorsPC extracts program counters from error values compatible with
|
||||
// the error types from golang.org/x/xerrors.
|
||||
//
|
||||
// It returns nil if err is not compatible with errors from that package or if
|
||||
// no program counters are stored in err.
|
||||
func extractXErrorsPC(err error) []uintptr {
|
||||
// This implementation uses the reflect package to avoid a hard dependency
|
||||
// on third-party packages.
|
||||
|
||||
// We don't know if err matches the expected type. For simplicity, instead
|
||||
// of trying to account for all possible ways things can go wrong, some
|
||||
// assumptions are made and if they are violated the code will panic. We
|
||||
// recover from any panic and ignore it, returning nil.
|
||||
//nolint: errcheck
|
||||
defer func() { recover() }()
|
||||
|
||||
field := reflect.ValueOf(err).Elem().FieldByName("frame") // type Frame struct{ frames [3]uintptr }
|
||||
field = field.FieldByName("frames")
|
||||
field = field.Slice(1, field.Len()) // drop first pc pointing to xerrors.New
|
||||
pc := make([]uintptr, field.Len())
|
||||
for i := 0; i < field.Len(); i++ {
|
||||
pc[i] = uintptr(field.Index(i).Uint())
|
||||
}
|
||||
return pc
|
||||
}
|
||||
|
||||
// Frame represents a function call and it's metadata. Frames are associated
|
||||
// with a Stacktrace.
|
||||
type Frame struct {
|
||||
Function string `json:"function,omitempty"`
|
||||
Symbol string `json:"symbol,omitempty"`
|
||||
// Module is, despite the name, the Sentry protocol equivalent of a Go
|
||||
// package's import path.
|
||||
Module string `json:"module,omitempty"`
|
||||
Filename string `json:"filename,omitempty"`
|
||||
AbsPath string `json:"abs_path,omitempty"`
|
||||
Lineno int `json:"lineno,omitempty"`
|
||||
Colno int `json:"colno,omitempty"`
|
||||
PreContext []string `json:"pre_context,omitempty"`
|
||||
ContextLine string `json:"context_line,omitempty"`
|
||||
PostContext []string `json:"post_context,omitempty"`
|
||||
InApp bool `json:"in_app"`
|
||||
Vars map[string]interface{} `json:"vars,omitempty"`
|
||||
// Package and the below are not used for Go stack trace frames. In
|
||||
// other platforms it refers to a container where the Module can be
|
||||
// found. For example, a Java JAR, a .NET Assembly, or a native
|
||||
// dynamic library. They exists for completeness, allowing the
|
||||
// construction and reporting of custom event payloads.
|
||||
Package string `json:"package,omitempty"`
|
||||
InstructionAddr string `json:"instruction_addr,omitempty"`
|
||||
AddrMode string `json:"addr_mode,omitempty"`
|
||||
SymbolAddr string `json:"symbol_addr,omitempty"`
|
||||
ImageAddr string `json:"image_addr,omitempty"`
|
||||
Platform string `json:"platform,omitempty"`
|
||||
StackStart bool `json:"stack_start,omitempty"`
|
||||
}
|
||||
|
||||
// NewFrame assembles a stacktrace frame out of runtime.Frame.
|
||||
func NewFrame(f runtime.Frame) Frame {
|
||||
function := f.Function
|
||||
var pkg string
|
||||
|
||||
if function != "" {
|
||||
pkg, function = splitQualifiedFunctionName(function)
|
||||
}
|
||||
|
||||
return newFrame(pkg, function, f.File, f.Line)
|
||||
}
|
||||
|
||||
// Like filepath.IsAbs() but doesn't care what platform you run this on.
|
||||
// I.e. it also recognizies `/path/to/file` when run on Windows.
|
||||
func isAbsPath(path string) bool {
|
||||
if len(path) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// If the volume name starts with a double slash, this is an absolute path.
|
||||
if len(path) >= 1 && (path[0] == '/' || path[0] == '\\') {
|
||||
return true
|
||||
}
|
||||
|
||||
// Windows absolute path, see https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats
|
||||
if len(path) >= 3 && path[1] == ':' && (path[2] == '/' || path[2] == '\\') {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func newFrame(module string, function string, file string, line int) Frame {
|
||||
frame := Frame{
|
||||
Lineno: line,
|
||||
Module: module,
|
||||
Function: function,
|
||||
}
|
||||
|
||||
switch {
|
||||
case len(file) == 0:
|
||||
frame.Filename = unknown
|
||||
// Leave abspath as the empty string to be omitted when serializing event as JSON.
|
||||
case isAbsPath(file):
|
||||
frame.AbsPath = file
|
||||
// TODO: in the general case, it is not trivial to come up with a
|
||||
// "project relative" path with the data we have in run time.
|
||||
// We shall not use filepath.Base because it creates ambiguous paths and
|
||||
// affects the "Suspect Commits" feature.
|
||||
// For now, leave relpath empty to be omitted when serializing the event
|
||||
// as JSON. Improve this later.
|
||||
default:
|
||||
// f.File is a relative path. This may happen when the binary is built
|
||||
// with the -trimpath flag.
|
||||
frame.Filename = file
|
||||
// Omit abspath when serializing the event as JSON.
|
||||
}
|
||||
|
||||
setInAppFrame(&frame)
|
||||
|
||||
return frame
|
||||
}
|
||||
|
||||
// splitQualifiedFunctionName splits a package path-qualified function name into
|
||||
// package name and function name. Such qualified names are found in
|
||||
// runtime.Frame.Function values.
|
||||
func splitQualifiedFunctionName(name string) (pkg string, fun string) {
|
||||
pkg = packageName(name)
|
||||
if len(pkg) > 0 {
|
||||
fun = name[len(pkg)+1:]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func extractFrames(pcs []uintptr) []runtime.Frame {
|
||||
var frames = make([]runtime.Frame, 0, len(pcs))
|
||||
callersFrames := runtime.CallersFrames(pcs)
|
||||
|
||||
for {
|
||||
callerFrame, more := callersFrames.Next()
|
||||
|
||||
frames = append(frames, callerFrame)
|
||||
|
||||
if !more {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
slices.Reverse(frames)
|
||||
return frames
|
||||
}
|
||||
|
||||
// createFrames creates Frame objects while filtering out frames that are not
|
||||
// meant to be reported to Sentry, those are frames internal to the SDK or Go.
|
||||
func createFrames(frames []runtime.Frame) []Frame {
|
||||
if len(frames) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := make([]Frame, 0, len(frames))
|
||||
|
||||
for _, frame := range frames {
|
||||
function := frame.Function
|
||||
var pkg string
|
||||
if function != "" {
|
||||
pkg, function = splitQualifiedFunctionName(function)
|
||||
}
|
||||
|
||||
if !shouldSkipFrame(pkg) {
|
||||
result = append(result, newFrame(pkg, function, frame.File, frame.Line))
|
||||
}
|
||||
}
|
||||
|
||||
// Fix issues grouping errors with the new fully qualified function names
|
||||
// introduced from Go 1.21
|
||||
result = cleanupFunctionNamePrefix(result)
|
||||
return result
|
||||
}
|
||||
|
||||
// TODO ID: why do we want to do this?
|
||||
// I'm not aware of other SDKs skipping all Sentry frames, regardless of their position in the stactrace.
|
||||
// For example, in the .NET SDK, only the first frames are skipped until the call to the SDK.
|
||||
// As is, this will also hide any intermediate frames in the stack and make debugging issues harder.
|
||||
func shouldSkipFrame(module string) bool {
|
||||
// Skip Go internal frames.
|
||||
if module == "runtime" || module == "testing" {
|
||||
return true
|
||||
}
|
||||
|
||||
// Skip Sentry internal frames, except for frames in _test packages (for testing).
|
||||
if strings.HasPrefix(module, "github.com/getsentry/sentry-go") &&
|
||||
!strings.HasSuffix(module, "_test") {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// On Windows, GOROOT has backslashes, but we want forward slashes.
|
||||
var goRoot = strings.ReplaceAll(build.Default.GOROOT, "\\", "/")
|
||||
|
||||
func setInAppFrame(frame *Frame) {
|
||||
frame.InApp = true
|
||||
if strings.HasPrefix(frame.AbsPath, goRoot) || strings.Contains(frame.Module, "vendor") ||
|
||||
strings.Contains(frame.Module, "third_party") {
|
||||
frame.InApp = false
|
||||
}
|
||||
}
|
||||
|
||||
func callerFunctionName() string {
|
||||
pcs := make([]uintptr, 1)
|
||||
runtime.Callers(3, pcs)
|
||||
callersFrames := runtime.CallersFrames(pcs)
|
||||
callerFrame, _ := callersFrames.Next()
|
||||
return baseName(callerFrame.Function)
|
||||
}
|
||||
|
||||
// packageName returns the package part of the symbol name, or the empty string
|
||||
// if there is none.
|
||||
// It replicates https://golang.org/pkg/debug/gosym/#Sym.PackageName, avoiding a
|
||||
// dependency on debug/gosym.
|
||||
func packageName(name string) string {
|
||||
if isCompilerGeneratedSymbol(name) {
|
||||
return ""
|
||||
}
|
||||
|
||||
pathend := strings.LastIndex(name, "/")
|
||||
if pathend < 0 {
|
||||
pathend = 0
|
||||
}
|
||||
|
||||
if i := strings.Index(name[pathend:], "."); i != -1 {
|
||||
return name[:pathend+i]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// baseName returns the symbol name without the package or receiver name.
|
||||
// It replicates https://golang.org/pkg/debug/gosym/#Sym.BaseName, avoiding a
|
||||
// dependency on debug/gosym.
|
||||
func baseName(name string) string {
|
||||
if i := strings.LastIndex(name, "."); i != -1 {
|
||||
return name[i+1:]
|
||||
}
|
||||
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
|
||||
}
|
||||
19
vendor/github.com/getsentry/sentry-go/traces_sampler.go
generated
vendored
Normal file
19
vendor/github.com/getsentry/sentry-go/traces_sampler.go
generated
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
package sentry
|
||||
|
||||
// A SamplingContext is passed to a TracesSampler to determine a sampling
|
||||
// decision.
|
||||
//
|
||||
// TODO(tracing): possibly expand SamplingContext to include custom /
|
||||
// user-provided data.
|
||||
type SamplingContext struct {
|
||||
Span *Span // The current span, always non-nil.
|
||||
Parent *Span // The parent span, may be nil.
|
||||
}
|
||||
|
||||
// The TracesSample type is an adapter to allow the use of ordinary
|
||||
// functions as a TracesSampler.
|
||||
type TracesSampler func(ctx SamplingContext) float64
|
||||
|
||||
func (f TracesSampler) Sample(ctx SamplingContext) float64 {
|
||||
return f(ctx)
|
||||
}
|
||||
1045
vendor/github.com/getsentry/sentry-go/tracing.go
generated
vendored
Normal file
1045
vendor/github.com/getsentry/sentry-go/tracing.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
732
vendor/github.com/getsentry/sentry-go/transport.go
generated
vendored
Normal file
732
vendor/github.com/getsentry/sentry-go/transport.go
generated
vendored
Normal file
@@ -0,0 +1,732 @@
|
||||
package sentry
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/getsentry/sentry-go/internal/ratelimit"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultBufferSize = 1000
|
||||
defaultTimeout = time.Second * 30
|
||||
)
|
||||
|
||||
// maxDrainResponseBytes is the maximum number of bytes that transport
|
||||
// implementations will read from response bodies when draining them.
|
||||
//
|
||||
// Sentry's ingestion API responses are typically short and the SDK doesn't need
|
||||
// the contents of the response body. However, the net/http HTTP client requires
|
||||
// response bodies to be fully drained (and closed) for TCP keep-alive to work.
|
||||
//
|
||||
// maxDrainResponseBytes strikes a balance between reading too much data (if the
|
||||
// server is misbehaving) and reusing TCP connections.
|
||||
const maxDrainResponseBytes = 16 << 10
|
||||
|
||||
// Transport is used by the Client to deliver events to remote server.
|
||||
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) {
|
||||
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
|
||||
}
|
||||
|
||||
func getRequestBodyFromEvent(event *Event) []byte {
|
||||
body, err := json.Marshal(event)
|
||||
if err == nil {
|
||||
return body
|
||||
}
|
||||
|
||||
msg := fmt.Sprintf("Could not encode original event as JSON. "+
|
||||
"Succeeded by removing Breadcrumbs, Contexts and Extra. "+
|
||||
"Please verify the data you attach to the scope. "+
|
||||
"Error: %s", err)
|
||||
// Try to serialize the event, with all the contextual data that allows for interface{} stripped.
|
||||
event.Breadcrumbs = nil
|
||||
event.Contexts = nil
|
||||
event.Extra = map[string]interface{}{
|
||||
"info": msg,
|
||||
}
|
||||
body, err = json.Marshal(event)
|
||||
if err == nil {
|
||||
DebugLogger.Println(msg)
|
||||
return body
|
||||
}
|
||||
|
||||
// This should _only_ happen when Event.Exception[0].Stacktrace.Frames[0].Vars is unserializable
|
||||
// Which won't ever happen, as we don't use it now (although it's the part of public interface accepted by Sentry)
|
||||
// Juuust in case something, somehow goes utterly wrong.
|
||||
DebugLogger.Println("Event couldn't be marshaled, even with stripped contextual data. Skipping delivery. " +
|
||||
"Please notify the SDK owners with possibly broken payload.")
|
||||
return nil
|
||||
}
|
||||
|
||||
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 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, itemsLength 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: itemsLength,
|
||||
ContentType: logEvent.ContentType,
|
||||
})
|
||||
if err == nil {
|
||||
err = enc.Encode(body)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
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 := enc.Encode(struct {
|
||||
EventID EventID `json:"event_id"`
|
||||
SentAt time.Time `json:"sent_at"`
|
||||
Dsn string `json:"dsn"`
|
||||
Sdk map[string]string `json:"sdk"`
|
||||
Trace map[string]string `json:"trace,omitempty"`
|
||||
}{
|
||||
EventID: event.EventID,
|
||||
SentAt: sentAt,
|
||||
Trace: trace,
|
||||
Dsn: dsn.String(),
|
||||
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)
|
||||
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
|
||||
}
|
||||
|
||||
func getRequestFromEvent(ctx context.Context, event *Event, dsn *Dsn) (r *http.Request, err error) {
|
||||
defer func() {
|
||||
if r != nil {
|
||||
r.Header.Set("User-Agent", fmt.Sprintf("%s/%s", event.Sdk.Name, event.Sdk.Version))
|
||||
r.Header.Set("Content-Type", "application/x-sentry-envelope")
|
||||
|
||||
auth := fmt.Sprintf("Sentry sentry_version=%s, "+
|
||||
"sentry_client=%s/%s, sentry_key=%s", apiVersion, event.Sdk.Name, event.Sdk.Version, dsn.publicKey)
|
||||
|
||||
// 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 passed through to Sentry if set.
|
||||
if dsn.secretKey != "" {
|
||||
auth = fmt.Sprintf("%s, sentry_secret=%s", auth, dsn.secretKey)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
return http.NewRequestWithContext(
|
||||
ctx,
|
||||
http.MethodPost,
|
||||
dsn.GetAPIURL().String(),
|
||||
envelope,
|
||||
)
|
||||
}
|
||||
|
||||
func categoryFor(eventType string) ratelimit.Category {
|
||||
switch eventType {
|
||||
case "":
|
||||
return ratelimit.CategoryError
|
||||
case transactionType:
|
||||
return ratelimit.CategoryTransaction
|
||||
default:
|
||||
return ratelimit.Category(eventType)
|
||||
}
|
||||
}
|
||||
|
||||
// ================================
|
||||
// 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 {
|
||||
request *http.Request
|
||||
category ratelimit.Category
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
// 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
|
||||
|
||||
start 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{}),
|
||||
}
|
||||
return &transport
|
||||
}
|
||||
|
||||
// Configure is called by the Client itself, providing it it's own ClientOptions.
|
||||
func (t *HTTPTransport) Configure(options ClientOptions) {
|
||||
dsn, err := NewDsn(options.Dsn)
|
||||
if err != nil {
|
||||
DebugLogger.Printf("%v\n", err)
|
||||
return
|
||||
}
|
||||
t.dsn = dsn
|
||||
|
||||
// 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.start.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 := categoryFor(event.Type)
|
||||
|
||||
if t.disabled(category) {
|
||||
return
|
||||
}
|
||||
|
||||
request, err := getRequestFromEvent(ctx, event, t.dsn)
|
||||
if err != nil {
|
||||
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
|
||||
|
||||
select {
|
||||
case b.items <- batchItem{
|
||||
request: request,
|
||||
category: category,
|
||||
}:
|
||||
var eventType string
|
||||
if event.Type == transactionType {
|
||||
eventType = "transaction"
|
||||
} else {
|
||||
eventType = fmt.Sprintf("%s event", event.Level)
|
||||
}
|
||||
DebugLogger.Printf(
|
||||
"Sending %s [%s] to %s project: %s",
|
||||
eventType,
|
||||
event.EventID,
|
||||
t.dsn.host,
|
||||
t.dsn.projectID,
|
||||
)
|
||||
default:
|
||||
DebugLogger.Println("Event dropped due to transport buffer being full.")
|
||||
}
|
||||
|
||||
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 {
|
||||
toolate := time.After(timeout)
|
||||
|
||||
// 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 <-toolate:
|
||||
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:
|
||||
DebugLogger.Println("Buffer flushed successfully.")
|
||||
return true
|
||||
case <-toolate:
|
||||
goto fail
|
||||
}
|
||||
|
||||
fail:
|
||||
DebugLogger.Println("Buffer flushing reached the timeout.")
|
||||
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.
|
||||
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 item, open := <-b.items:
|
||||
if !open {
|
||||
break loop
|
||||
}
|
||||
if t.disabled(item.category) {
|
||||
continue
|
||||
}
|
||||
|
||||
response, err := t.client.Do(item.request)
|
||||
if err != nil {
|
||||
DebugLogger.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 {
|
||||
DebugLogger.Printf("Error while reading response code: %v", err)
|
||||
}
|
||||
DebugLogger.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()
|
||||
}
|
||||
}
|
||||
|
||||
// Signal that processing of the batch is done.
|
||||
close(b.done)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *HTTPTransport) disabled(c ratelimit.Category) bool {
|
||||
t.mu.RLock()
|
||||
defer t.mu.RUnlock()
|
||||
disabled := t.limits.IsRateLimited(c)
|
||||
if disabled {
|
||||
DebugLogger.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
|
||||
|
||||
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 it it's own ClientOptions.
|
||||
func (t *HTTPSyncTransport) Configure(options ClientOptions) {
|
||||
dsn, err := NewDsn(options.Dsn)
|
||||
if err != nil {
|
||||
DebugLogger.Printf("%v\n", err)
|
||||
return
|
||||
}
|
||||
t.dsn = dsn
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if t.disabled(categoryFor(event.Type)) {
|
||||
return
|
||||
}
|
||||
|
||||
request, err := getRequestFromEvent(ctx, event, t.dsn)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var eventIdentifier string
|
||||
switch event.Type {
|
||||
case transactionType:
|
||||
eventIdentifier = "transaction"
|
||||
case logEvent.Type:
|
||||
eventIdentifier = fmt.Sprintf("%v log events", len(event.Logs))
|
||||
default:
|
||||
eventIdentifier = fmt.Sprintf("%s event", event.Level)
|
||||
}
|
||||
DebugLogger.Printf(
|
||||
"Sending %s [%s] to %s project: %s",
|
||||
eventIdentifier,
|
||||
event.EventID,
|
||||
t.dsn.host,
|
||||
t.dsn.projectID,
|
||||
)
|
||||
|
||||
response, err := t.client.Do(request)
|
||||
if err != nil {
|
||||
DebugLogger.Printf("There was an issue with sending an event: %v", err)
|
||||
return
|
||||
}
|
||||
if response.StatusCode >= 400 && response.StatusCode <= 599 {
|
||||
b, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
DebugLogger.Printf("Error while reading response code: %v", err)
|
||||
}
|
||||
DebugLogger.Printf("Sending %s failed with the following error: %s", eventIdentifier, 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()
|
||||
}
|
||||
|
||||
// Flush is a no-op for HTTPSyncTransport. It always returns true immediately.
|
||||
func (t *HTTPSyncTransport) Flush(_ time.Duration) 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 {
|
||||
DebugLogger.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) {
|
||||
DebugLogger.Println("Sentry client initialized with an empty DSN. Using noopTransport. No events will be delivered.")
|
||||
}
|
||||
|
||||
func (noopTransport) SendEvent(*Event) {
|
||||
DebugLogger.Println("Event dropped due to noopTransport usage.")
|
||||
}
|
||||
|
||||
func (noopTransport) Flush(time.Duration) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (noopTransport) Close() {}
|
||||
118
vendor/github.com/getsentry/sentry-go/util.go
generated
vendored
Normal file
118
vendor/github.com/getsentry/sentry-go/util.go
generated
vendored
Normal file
@@ -0,0 +1,118 @@
|
||||
package sentry
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
exec "golang.org/x/sys/execabs"
|
||||
)
|
||||
|
||||
func uuid() string {
|
||||
id := make([]byte, 16)
|
||||
// Prefer rand.Read over rand.Reader, see https://go-review.googlesource.com/c/go/+/272326/.
|
||||
_, _ = rand.Read(id)
|
||||
id[6] &= 0x0F // clear version
|
||||
id[6] |= 0x40 // set version to 4 (random uuid)
|
||||
id[8] &= 0x3F // clear variant
|
||||
id[8] |= 0x80 // set to IETF variant
|
||||
return hex.EncodeToString(id)
|
||||
}
|
||||
|
||||
func fileExists(fileName string) bool {
|
||||
_, err := os.Stat(fileName)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// monotonicTimeSince replaces uses of time.Now() to take into account the
|
||||
// monotonic clock reading stored in start, such that duration = end - start is
|
||||
// unaffected by changes in the system wall clock.
|
||||
func monotonicTimeSince(start time.Time) (end time.Time) {
|
||||
return start.Add(time.Since(start))
|
||||
}
|
||||
|
||||
// nolint: unused
|
||||
func prettyPrint(data interface{}) {
|
||||
dbg, _ := json.MarshalIndent(data, "", " ")
|
||||
fmt.Println(string(dbg))
|
||||
}
|
||||
|
||||
// defaultRelease attempts to guess a default release for the currently running
|
||||
// program.
|
||||
func defaultRelease() (release string) {
|
||||
// Return first non-empty environment variable known to hold release info, if any.
|
||||
envs := []string{
|
||||
"SENTRY_RELEASE",
|
||||
"HEROKU_SLUG_COMMIT",
|
||||
"SOURCE_VERSION",
|
||||
"CODEBUILD_RESOLVED_SOURCE_VERSION",
|
||||
"CIRCLE_SHA1",
|
||||
"GAE_DEPLOYMENT_ID",
|
||||
"GITHUB_SHA", // GitHub Actions - https://help.github.com/en/actions
|
||||
"COMMIT_REF", // Netlify - https://docs.netlify.com/
|
||||
"VERCEL_GIT_COMMIT_SHA", // Vercel - https://vercel.com/
|
||||
"ZEIT_GITHUB_COMMIT_SHA", // Zeit (now known as Vercel)
|
||||
"ZEIT_GITLAB_COMMIT_SHA",
|
||||
"ZEIT_BITBUCKET_COMMIT_SHA",
|
||||
}
|
||||
for _, e := range envs {
|
||||
if release = os.Getenv(e); release != "" {
|
||||
DebugLogger.Printf("Using release from environment variable %s: %s", e, release)
|
||||
return release
|
||||
}
|
||||
}
|
||||
|
||||
if info, ok := debug.ReadBuildInfo(); ok {
|
||||
buildInfoVcsRevision := revisionFromBuildInfo(info)
|
||||
if len(buildInfoVcsRevision) > 0 {
|
||||
return buildInfoVcsRevision
|
||||
}
|
||||
}
|
||||
|
||||
// Derive a version string from Git. Example outputs:
|
||||
// v1.0.1-0-g9de4
|
||||
// v2.0-8-g77df-dirty
|
||||
// 4f72d7
|
||||
if _, err := exec.LookPath("git"); err == nil {
|
||||
cmd := exec.Command("git", "describe", "--long", "--always", "--dirty")
|
||||
b, err := cmd.Output()
|
||||
if err != nil {
|
||||
// Either Git is not available or the current directory is not a
|
||||
// Git repository.
|
||||
var s strings.Builder
|
||||
fmt.Fprintf(&s, "Release detection failed: %v", err)
|
||||
if err, ok := err.(*exec.ExitError); ok && len(err.Stderr) > 0 {
|
||||
fmt.Fprintf(&s, ": %s", err.Stderr)
|
||||
}
|
||||
DebugLogger.Print(s.String())
|
||||
} else {
|
||||
release = strings.TrimSpace(string(b))
|
||||
DebugLogger.Printf("Using release from Git: %s", release)
|
||||
return release
|
||||
}
|
||||
}
|
||||
|
||||
DebugLogger.Print("Some Sentry features will not be available. See https://docs.sentry.io/product/releases/.")
|
||||
DebugLogger.Print("To stop seeing this message, pass a Release to sentry.Init or set the SENTRY_RELEASE environment variable.")
|
||||
return ""
|
||||
}
|
||||
|
||||
func revisionFromBuildInfo(info *debug.BuildInfo) string {
|
||||
for _, setting := range info.Settings {
|
||||
if setting.Key == "vcs.revision" && setting.Value != "" {
|
||||
DebugLogger.Printf("Using release from debug info: %s", setting.Value)
|
||||
return setting.Value
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func Pointer[T any](v T) *T {
|
||||
return &v
|
||||
}
|
||||
Reference in New Issue
Block a user