Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(deps): update module github.com/getsentry/sentry-go to v0.31.1 #233

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

renovate[bot]
Copy link
Contributor

@renovate renovate bot commented Dec 11, 2024

This PR contains the following updates:

Package Change Age Adoption Passing Confidence
github.com/getsentry/sentry-go v0.29.1 -> v0.31.1 age adoption passing confidence

Release Notes

getsentry/sentry-go (github.com/getsentry/sentry-go)

v0.31.1: 0.31.1

Compare Source

The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.31.1.

Bug Fixes
  • Correct wrong module name for sentry-go/logrus (#​950)

v0.31.0: 0.31.0

Compare Source

Breaking Changes
  • Remove support for metrics. Read more about the end of the Metrics beta here. (#​914)

  • Remove support for profiling. (#​915)

  • Remove Segment field from the User struct. This field is no longer used in the Sentry product. (#​928)

  • Every integration is now a separate module, reducing the binary size and number of dependencies. Once you update sentry-go to latest version, you'll need to go get the integration you want to use. For example, if you want to use the echo integration, you'll need to run go get github.com/getsentry/sentry-go/echo (#​919).

Features
  • Add the ability to override hub in context for integrations that use custom context. (#​931)

  • Add HubProvider Hook for sentrylogrus, enabling dynamic Sentry hub allocation for each log entry or goroutine. (#​936)

This change enhances compatibility with Sentry's recommendation of using separate hubs per goroutine. To ensure a separate Sentry hub for each goroutine, configure the HubProvider like this:

hook, err := sentrylogrus.New(nil, sentry.ClientOptions{})
if err != nil {
    log.Fatalf("Failed to initialize Sentry hook: %v", err)
}

// Set a custom HubProvider to generate a new hub for each goroutine or log entry
hook.SetHubProvider(func() *sentry.Hub {
    client, _ := sentry.NewClient(sentry.ClientOptions{})
    return sentry.NewHub(client, sentry.NewScope())
})

logrus.AddHook(hook)
Bug Fixes
  • Add support for closing worker goroutines started by the HTTPTranport to prevent goroutine leaks. (#​894)
client, _ := sentry.NewClient()
defer client.Close()

Worker can be also closed by calling Close() method on the HTTPTransport instance. Close should be called after Flush and before terminating the program otherwise some events may be lost.

transport := sentry.NewHTTPTransport()
defer transport.Close()
Misc

v0.30.0: 0.30.0

Compare Source

The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.30.0.

Features
  • Add sentryzerolog integration (#​857)
  • Add sentryslog integration (#​865)
  • Always set Mechanism Type to generic (#​896)
Bug Fixes
  • Prevent panic in fasthttp and fiber integration in case a malformed URL has to be parsed (#​912)
Misc

Drop support for Go 1.18, 1.19 and 1.20. The currently supported Go versions are the last 3 stable releases: 1.23, 1.22 and 1.21.


Configuration

📅 Schedule: Branch creation - "* 0-12 * * 3" (UTC), Automerge - At any time (no schedule defined).

🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.

Rebasing: Whenever PR is behind base branch, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about this update again.


  • If you want to rebase/retry this PR, check this box

This PR was generated by Mend Renovate. View the repository job log.

@renovate renovate bot force-pushed the renovate/github.com-getsentry-sentry-go-0.x branch 2 times, most recently from 2bb5fa8 to 9856617 Compare December 29, 2024 17:35
Copy link

[puLL-Merge] - getsentry/sentry-go@otel/v0.29.1..otel/v0.30.0

Diff
diff --git .craft.yml .craft.yml
index 238288e45..25ecf6855 100644
--- .craft.yml
+++ .craft.yml
@@ -8,6 +8,12 @@ targets:
   - name: github
     tagPrefix: otel/v
     tagOnly: true
+  - name: github
+    tagPrefix: slog/v
+    tagOnly: true
+  - name: github
+    tagPrefix: zerolog/v
+    tagOnly: true
   - name: registry
     sdks:
       github:getsentry/sentry-go:
diff --git .github/workflows/lint.yml .github/workflows/lint.yml
index 6a359f221..68b823ef5 100644
--- .github/workflows/lint.yml
+++ .github/workflows/lint.yml
@@ -20,7 +20,7 @@ jobs:
     steps:
       - uses: actions/setup-go@v5
         with:
-          go-version: "1.22"
+          go-version: "1.23"
       - uses: actions/checkout@v4
       - name: golangci-lint
         uses: golangci/golangci-lint-action@971e284b6050e8a5849b72094c50ab08da042db8 # pin@v6.1.1
diff --git .github/workflows/test.yml .github/workflows/test.yml
index 04e61db39..394eca7eb 100644
--- .github/workflows/test.yml
+++ .github/workflows/test.yml
@@ -46,11 +46,11 @@ jobs:
         run: make vet
       - name: Check go.mod Tidiness
         run: make mod-tidy
-        if: ${{ matrix.go == '1.20' }}
+        if: ${{ matrix.go == '1.21' }}
       - name: Test
         run: make test-coverage
       - name: Upload coverage to Codecov
-        uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # pin@v4.6.0
+        uses: codecov/codecov-action@015f24e6818733317a2da2edd6290ab26238649a # pin@v5.0.7
         with:
           directory: .coverage
           token: ${{ secrets.CODECOV_TOKEN }}
@@ -64,6 +64,6 @@ jobs:
     timeout-minutes: 15
     strategy:
       matrix:
-        go: ["1.23", "1.22", "1.21", "1.20", "1.19", "1.18"]
+        go: ["1.23", "1.22", "1.21"]
         os: [ubuntu, windows, macos]
       fail-fast: false
diff --git CHANGELOG.md CHANGELOG.md
index 4818085cf..533d1dd01 100644
--- CHANGELOG.md
+++ CHANGELOG.md
@@ -1,5 +1,23 @@
 # Changelog
 
+## 0.30.0
+
+The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.30.0.
+
+### Features
+
+- Add `sentryzerolog` integration ([#857](https://github.com/getsentry/sentry-go/pull/857))
+- Add `sentryslog` integration ([#865](https://github.com/getsentry/sentry-go/pull/865))
+- Always set Mechanism Type to generic ([#896](https://github.com/getsentry/sentry-go/pull/897))
+
+### Bug Fixes
+
+- Prevent panic in `fasthttp` and `fiber` integration in case a malformed URL has to be parsed ([#912](https://github.com/getsentry/sentry-go/pull/912))
+
+### Misc
+
+Drop support for Go 1.18, 1.19 and 1.20. The currently supported Go versions are the last 3 stable releases: 1.23, 1.22 and 1.21.
+
 ## 0.29.1
 
 The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.29.1.
@@ -9,6 +27,10 @@ The Sentry SDK team is happy to announce the immediate availability of Sentry Go
 - Correlate errors to the current trace ([#886](https://github.com/getsentry/sentry-go/pull/886))
 - Set the trace context when the transaction finishes ([#888](https://github.com/getsentry/sentry-go/pull/888))
 
+### Misc
+
+- Update the `sentrynegroni` integration to use the latest (v3.1.1) version of Negroni ([#885](https://github.com/getsentry/sentry-go/pull/885))
+
 ## 0.29.0
 
 The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.29.0.
diff --git Makefile Makefile
index 89523ac0c..9cc0959fd 100644
--- Makefile
+++ Makefile
@@ -58,9 +58,8 @@ test-coverage: $(COVERAGE_REPORT_DIR) clean-report-dir  ## Test with coverage en
 mod-tidy: ## Check go.mod tidiness
 	set -e ; \
 	for dir in $(ALL_GO_MOD_DIRS); do \
-		cd "$${dir}"; \
 		echo ">>> Running 'go mod tidy' for module: $${dir}"; \
-		go mod tidy -go=1.18 -compat=1.18; \
+		(cd "$${dir}" && go mod tidy -go=1.21 -compat=1.21); \
 	done; \
 	git diff --exit-code;
 .PHONY: mod-tidy
@@ -68,12 +67,12 @@ mod-tidy: ## Check go.mod tidiness
 vet: ## Run "go vet"
 	set -e ; \
 	for dir in $(ALL_GO_MOD_DIRS); do \
-		cd "$${dir}"; \
 		echo ">>> Running 'go vet' for module: $${dir}"; \
-		go vet ./...; \
+		(cd "$${dir}" && go vet ./...); \
 	done;
 .PHONY: vet
 
+
 lint: ## Lint (using "golangci-lint")
 	golangci-lint run
 .PHONY: lint
diff --git README.md README.md
index 3b67fb45a..bbd9c4305 100644
--- README.md
+++ README.md
@@ -75,8 +75,10 @@ checkout the official documentation:
   - [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/)
 
 ## Resources
diff --git a/_examples/slog/main.go b/_examples/slog/main.go
new file mode 100644
index 000000000..4a62bf480
--- /dev/null
+++ _examples/slog/main.go
@@ -0,0 +1,38 @@
+package main
+
+import (
+	"fmt"
+	"log"
+	"time"
+
+	"log/slog"
+
+	"github.com/getsentry/sentry-go"
+	sentryslog "github.com/getsentry/sentry-go/slog"
+)
+
+func main() {
+	err := sentry.Init(sentry.ClientOptions{
+		Dsn:           "",
+		EnableTracing: false,
+	})
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	defer sentry.Flush(2 * time.Second)
+
+	logger := slog.New(sentryslog.Option{Level: slog.LevelDebug}.NewSentryHandler())
+	logger = logger.With("release", "v1.0.0")
+
+	logger.
+		With(
+			slog.Group("user",
+				slog.String("id", "user-123"),
+				slog.Time("created_at", time.Now()),
+			),
+		).
+		With("environment", "dev").
+		With("error", fmt.Errorf("an error")).
+		Error("a message")
+}
diff --git a/_examples/zerolog/main.go b/_examples/zerolog/main.go
new file mode 100644
index 000000000..c91dbce3d
--- /dev/null
+++ _examples/zerolog/main.go
@@ -0,0 +1,49 @@
+package main
+
+import (
+	"github.com/getsentry/sentry-go"
+	sentryzerolog "github.com/getsentry/sentry-go/zerolog"
+	"github.com/rs/zerolog"
+	"os"
+	"time"
+)
+
+func main() {
+	w, err := sentryzerolog.New(sentryzerolog.Config{
+		Options: sentryzerolog.Options{
+			Levels: []zerolog.Level{
+				zerolog.DebugLevel,
+				zerolog.ErrorLevel,
+				zerolog.FatalLevel,
+				zerolog.PanicLevel,
+			},
+			WithBreadcrumbs: true,
+			FlushTimeout:    5 * time.Second,
+		},
+		ClientOptions: sentry.ClientOptions{
+			Dsn:              "",
+			Environment:      "development",
+			Release:          "1.0",
+			Debug:            true,
+			AttachStacktrace: true,
+		},
+	})
+
+	if err != nil {
+		panic(err)
+	}
+
+	defer func() {
+		err = w.Close()
+		if err != nil {
+			panic(err)
+		}
+	}()
+
+	m := zerolog.MultiLevelWriter(os.Stdout, w)
+	logger := zerolog.New(m).With().Timestamp().Logger()
+
+	logger.Debug().Msg("Application has started")
+	logger.Error().Msg("Oh no!")
+	logger.Fatal().Msg("Can't continue...")
+}
diff --git client.go client.go
index b5b11b31d..3b46be985 100644
--- client.go
+++ client.go
@@ -590,14 +590,6 @@ func (client *Client) GetSDKIdentifier() string {
 	return client.sdkIdentifier
 }
 
-// reverse reverses the slice a in place.
-func reverse(a []Exception) {
-	for i := len(a)/2 - 1; i >= 0; i-- {
-		opp := len(a) - 1 - i
-		a[i], a[opp] = a[opp], a[i]
-	}
-}
-
 func (client *Client) processEvent(event *Event, hint *EventHint, scope EventModifier) *EventID {
 	if event == nil {
 		err := usageError{fmt.Errorf("%s called with nil event", callerFunctionName())}
diff --git client_test.go client_test.go
index 58dcaaca9..60d63e2ea 100644
--- client_test.go
+++ client_test.go
@@ -165,6 +165,7 @@ func TestCaptureException(t *testing.T) {
 					Type:  "*sentry.customErr",
 					Value: "wat",
 					Mechanism: &Mechanism{
+						Type:             "generic",
 						ExceptionID:      0,
 						IsExceptionGroup: true,
 					},
@@ -174,6 +175,7 @@ func TestCaptureException(t *testing.T) {
 					Value:      "wat",
 					Stacktrace: &Stacktrace{Frames: []Frame{}},
 					Mechanism: &Mechanism{
+						Type:             "generic",
 						ExceptionID:      1,
 						ParentID:         Pointer(0),
 						IsExceptionGroup: true,
@@ -200,6 +202,7 @@ func TestCaptureException(t *testing.T) {
 					Type:  "*sentry.customErr",
 					Value: "wat",
 					Mechanism: &Mechanism{
+						Type:             "generic",
 						ExceptionID:      0,
 						IsExceptionGroup: true,
 					},
@@ -209,6 +212,7 @@ func TestCaptureException(t *testing.T) {
 					Value:      "err",
 					Stacktrace: &Stacktrace{Frames: []Frame{}},
 					Mechanism: &Mechanism{
+						Type:             "generic",
 						ExceptionID:      1,
 						ParentID:         Pointer(0),
 						IsExceptionGroup: true,
@@ -224,6 +228,7 @@ func TestCaptureException(t *testing.T) {
 					Type:  "*errors.errorString",
 					Value: "original",
 					Mechanism: &Mechanism{
+						Type:             "generic",
 						ExceptionID:      0,
 						IsExceptionGroup: true,
 					},
@@ -233,6 +238,7 @@ func TestCaptureException(t *testing.T) {
 					Value:      "wrapped: original",
 					Stacktrace: &Stacktrace{Frames: []Frame{}},
 					Mechanism: &Mechanism{
+						Type:             "generic",
 						ExceptionID:      1,
 						ParentID:         Pointer(0),
 						IsExceptionGroup: true,
diff --git dynamic_sampling_context.go dynamic_sampling_context.go
index 6eec95ba1..8dae0838b 100644
--- dynamic_sampling_context.go
+++ dynamic_sampling_context.go
@@ -100,15 +100,17 @@ func (d DynamicSamplingContext) String() string {
 		}
 		members = append(members, member)
 	}
-	if len(members) > 0 {
-		baggage, err := baggage.New(members...)
-		if err != nil {
-			return ""
-		}
-		return baggage.String()
+
+	if len(members) == 0 {
+		return ""
+	}
+
+	baggage, err := baggage.New(members...)
+	if err != nil {
+		return ""
 	}
 
-	return ""
+	return baggage.String()
 }
 
 // Constructs a new DynamicSamplingContext using a scope and client. Accessing
diff --git fasthttp/sentryfasthttp.go fasthttp/sentryfasthttp.go
index bd8cc6261..4863bf861 100644
--- fasthttp/sentryfasthttp.go
+++ fasthttp/sentryfasthttp.go
@@ -13,11 +13,7 @@ import (
 	"github.com/valyala/fasthttp"
 )
 
-type contextKey int
-
 const (
-	ContextKey = contextKey(1)
-	// The identifier of the FastHTTP SDK.
 	sdkIdentifier  = "sentry.go.fasthttp"
 	valuesKey      = "sentry"
 	transactionKey = "sentry_transaction"
@@ -76,11 +72,9 @@ func (h *Handler) Handle(handler fasthttp.RequestHandler) fasthttp.RequestHandle
 			sentry.WithSpanOrigin(sentry.SpanOriginFastHTTP),
 		}
 
-		method := string(ctx.Method())
-
 		transaction := sentry.StartTransaction(
 			sentry.SetHubOnContext(ctx, hub),
-			fmt.Sprintf("%s %s", method, string(ctx.Path())),
+			fmt.Sprintf("%s %s", r.Method, string(ctx.Path())),
 			options...,
 		)
 		defer func() {
@@ -90,7 +84,7 @@ func (h *Handler) Handle(handler fasthttp.RequestHandler) fasthttp.RequestHandle
 			transaction.Finish()
 		}()
 
-		transaction.SetData("http.request.method", method)
+		transaction.SetData("http.request.method", r.Method)
 
 		scope := hub.Scope()
 		scope.SetRequest(r)
@@ -146,17 +140,23 @@ func convert(ctx *fasthttp.RequestCtx) *http.Request {
 	r := new(http.Request)
 
 	r.Method = string(ctx.Method())
+
 	uri := ctx.URI()
-	// Ignore error.
-	r.URL, _ = url.Parse(fmt.Sprintf("%s://%s%s", uri.Scheme(), uri.Host(), uri.Path()))
+	url, err := url.Parse(fmt.Sprintf("%s://%s%s", uri.Scheme(), uri.Host(), uri.Path()))
+	if err == nil {
+		r.URL = url
+		r.URL.RawQuery = string(uri.QueryString())
+	}
+
+	host := string(ctx.Host())
+	r.Host = host
 
 	// Headers
 	r.Header = make(http.Header)
-	r.Header.Add("Host", string(ctx.Host()))
+	r.Header.Add("Host", host)
 	ctx.Request.Header.VisitAll(func(key, value []byte) {
 		r.Header.Add(string(key), string(value))
 	})
-	r.Host = string(ctx.Host())
 
 	// Cookies
 	ctx.Request.Header.VisitAllCookie(func(key, value []byte) {
@@ -166,9 +166,6 @@ func convert(ctx *fasthttp.RequestCtx) *http.Request {
 	// Env
 	r.RemoteAddr = ctx.RemoteAddr().String()
 
-	// QueryString
-	r.URL.RawQuery = string(ctx.URI().QueryString())
-
 	// Body
 	r.Body = io.NopCloser(bytes.NewReader(ctx.Request.Body()))
 
diff --git fiber/sentryfiber.go fiber/sentryfiber.go
index 3586e192d..ad427c566 100644
--- fiber/sentryfiber.go
+++ fiber/sentryfiber.go
@@ -65,8 +65,6 @@ func (h *handler) handle(ctx *fiber.Ctx) error {
 
 	r := convert(ctx)
 
-	method := ctx.Method()
-
 	transactionName := ctx.Path()
 	transactionSource := sentry.SourceURL
 
@@ -79,7 +77,7 @@ func (h *handler) handle(ctx *fiber.Ctx) error {
 
 	transaction := sentry.StartTransaction(
 		sentry.SetHubOnContext(ctx.Context(), hub),
-		fmt.Sprintf("%s %s", method, transactionName),
+		fmt.Sprintf("%s %s", r.Method, transactionName),
 		options...,
 	)
 
@@ -90,7 +88,7 @@ func (h *handler) handle(ctx *fiber.Ctx) error {
 		transaction.Finish()
 	}()
 
-	transaction.SetData("http.request.method", method)
+	transaction.SetData("http.request.method", r.Method)
 
 	scope := hub.Scope()
 	scope.SetRequest(r)
@@ -141,22 +139,33 @@ func convert(ctx *fiber.Ctx) *http.Request {
 	r := new(http.Request)
 
 	r.Method = utils.CopyString(ctx.Method())
+
 	uri := ctx.Request().URI()
-	r.URL, _ = url.Parse(fmt.Sprintf("%s://%s%s", uri.Scheme(), uri.Host(), uri.Path()))
+	url, err := url.Parse(fmt.Sprintf("%s://%s%s", uri.Scheme(), uri.Host(), uri.Path()))
+	if err == nil {
+		r.URL = url
+		r.URL.RawQuery = string(uri.QueryString())
+	}
+
+	host := utils.CopyString(ctx.Hostname())
+	r.Host = host
 
 	// Headers
 	r.Header = make(http.Header)
+	r.Header.Add("Host", host)
+
 	ctx.Request().Header.VisitAll(func(key, value []byte) {
 		r.Header.Add(string(key), string(value))
 	})
-	r.Host = utils.CopyString(ctx.Hostname())
+
+	// Cookies
+	ctx.Request().Header.VisitAllCookie(func(key, value []byte) {
+		r.AddCookie(&http.Cookie{Name: string(key), Value: string(value)})
+	})
 
 	// Env
 	r.RemoteAddr = ctx.Context().RemoteAddr().String()
 
-	// QueryString
-	r.URL.RawQuery = string(ctx.Request().URI().QueryString())
-
 	// Body
 	r.Body = io.NopCloser(bytes.NewReader(ctx.Request().Body()))
 
diff --git go.mod go.mod
index 7c7a31eaa..c06650d81 100644
--- go.mod
+++ go.mod
@@ -1,6 +1,6 @@
 module github.com/getsentry/sentry-go
 
-go 1.18
+go 1.21
 
 require (
 	github.com/gin-gonic/gin v1.8.1
diff --git go.sum go.sum
index 2144722a8..a01095fe7 100644
--- go.sum
+++ go.sum
@@ -130,7 +130,9 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLA
 github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY=
 github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc=
 github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
+github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
 github.com/onsi/gomega v1.27.1 h1:rfztXRbg6nv/5f+Raen9RcGoSecHIFgBBLQK3Wdj754=
+github.com/onsi/gomega v1.27.1/go.mod h1:aHX5xOykVYzWOV4WqQy0sy8BQptgukenXpCXfadcIAw=
 github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg=
 github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas=
 github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
@@ -212,6 +214,7 @@ github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FB
 github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M=
 github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
 github.com/yudai/pp v2.0.1+incompatible h1:Q4//iY4pNF6yPLZIigmvcl7k/bPgrcTPIFIcmawg5bI=
+github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
diff --git hub.go hub.go
index c99b6d70d..04cebee3e 100644
--- hub.go
+++ hub.go
@@ -294,8 +294,13 @@ func (hub *Hub) AddBreadcrumb(breadcrumb *Breadcrumb, hint *BreadcrumbHint) {
 	}
 
 	max := client.options.MaxBreadcrumbs
-	if max < 0 {
+	switch {
+	case max < 0:
 		return
+	case max == 0:
+		max = defaultMaxBreadcrumbs
+	case max > maxBreadcrumbs:
+		max = maxBreadcrumbs
 	}
 
 	if client.options.BeforeBreadcrumb != nil {
@@ -308,12 +313,6 @@ func (hub *Hub) AddBreadcrumb(breadcrumb *Breadcrumb, hint *BreadcrumbHint) {
 		}
 	}
 
-	if max == 0 {
-		max = defaultMaxBreadcrumbs
-	} else if max > maxBreadcrumbs {
-		max = maxBreadcrumbs
-	}
-
 	hub.Scope().AddBreadcrumb(breadcrumb, max)
 }
 
diff --git hub_test.go hub_test.go
index 0b306a0a3..93381d2a9 100644
--- hub_test.go
+++ hub_test.go
@@ -3,6 +3,7 @@ package sentry
 import (
 	"context"
 	"fmt"
+	"slices"
 	"strings"
 	"sync"
 	"testing"
@@ -400,7 +401,7 @@ func TestGetBaggage(t *testing.T) {
 		t.Run(name, func(t *testing.T) {
 			result := tt.hub.GetBaggage()
 			res := strings.Split(result, ",")
-			sortSlice(res)
+			slices.Sort(res)
 			assertEqual(t, strings.Join(res, ","), tt.expected)
 		})
 	}
diff --git interfaces.go interfaces.go
index cacb25436..894a9a4db 100644
--- interfaces.go
+++ interfaces.go
@@ -8,6 +8,7 @@ import (
 	"net"
 	"net/http"
 	"reflect"
+	"slices"
 	"strings"
 	"time"
 )
@@ -123,27 +124,27 @@ type User struct {
 }
 
 func (u User) IsEmpty() bool {
-	if len(u.ID) > 0 {
+	if u.ID != "" {
 		return false
 	}
 
-	if len(u.Email) > 0 {
+	if u.Email != "" {
 		return false
 	}
 
-	if len(u.IPAddress) > 0 {
+	if u.IPAddress != "" {
 		return false
 	}
 
-	if len(u.Username) > 0 {
+	if u.Username != "" {
 		return false
 	}
 
-	if len(u.Name) > 0 {
+	if u.Name != "" {
 		return false
 	}
 
-	if len(u.Segment) > 0 {
+	if u.Segment != "" {
 		return false
 	}
 
@@ -238,8 +239,7 @@ type Mechanism struct {
 // SetUnhandled indicates that the exception is an unhandled exception, i.e.
 // from a panic.
 func (m *Mechanism) SetUnhandled() {
-	h := false
-	m.Handled = &h
+	m.Handled = Pointer(false)
 }
 
 // Exception specifies an error that occurred.
@@ -398,12 +398,13 @@ func (e *Event) SetException(exception error, maxErrorDepth int) {
 	}
 
 	// event.Exception should be sorted such that the most recent error is last.
-	reverse(e.Exception)
+	slices.Reverse(e.Exception)
 
 	for i := range e.Exception {
 		e.Exception[i].Mechanism = &Mechanism{
 			IsExceptionGroup: true,
 			ExceptionID:      i,
+			Type:             "generic",
 		}
 		if i == 0 {
 			continue
@@ -429,7 +430,9 @@ func (e *Event) MarshalJSON() ([]byte, error) {
 	// and a few type tricks.
 	if e.Type == transactionType {
 		return e.transactionMarshalJSON()
-	} else if e.Type == checkInType {
+	}
+
+	if e.Type == checkInType {
 		return e.checkInMarshalJSON()
 	}
 	return e.defaultMarshalJSON()
diff --git interfaces_test.go interfaces_test.go
index 718fcfaa0..bf7b96ca1 100644
--- interfaces_test.go
+++ interfaces_test.go
@@ -260,6 +260,7 @@ func TestSetException(t *testing.T) {
 					Value: "base error",
 					Type:  "*errors.errorString",
 					Mechanism: &Mechanism{
+						Type:             "generic",
 						ExceptionID:      0,
 						IsExceptionGroup: true,
 					},
@@ -268,6 +269,7 @@ func TestSetException(t *testing.T) {
 					Value: "level 1: base error",
 					Type:  "*fmt.wrapError",
 					Mechanism: &Mechanism{
+						Type:             "generic",
 						ExceptionID:      1,
 						ParentID:         Pointer(0),
 						IsExceptionGroup: true,
@@ -278,6 +280,7 @@ func TestSetException(t *testing.T) {
 					Type:       "*fmt.wrapError",
 					Stacktrace: &Stacktrace{Frames: []Frame{}},
 					Mechanism: &Mechanism{
+						Type:             "generic",
 						ExceptionID:      2,
 						ParentID:         Pointer(1),
 						IsExceptionGroup: true,
@@ -309,6 +312,7 @@ func TestSetException(t *testing.T) {
 					Value: "the cause",
 					Type:  "*errors.errorString",
 					Mechanism: &Mechanism{
+						Type:             "generic",
 						ExceptionID:      0,
 						IsExceptionGroup: true,
 					},
@@ -317,6 +321,7 @@ func TestSetException(t *testing.T) {
 					Value: "error with cause",
 					Type:  "*sentry.withCause",
 					Mechanism: &Mechanism{
+						Type:             "generic",
 						ExceptionID:      1,
 						ParentID:         Pointer(0),
 						IsExceptionGroup: true,
@@ -327,6 +332,7 @@ func TestSetException(t *testing.T) {
 					Type:       "*fmt.wrapError",
 					Stacktrace: &Stacktrace{Frames: []Frame{}},
 					Mechanism: &Mechanism{
+						Type:             "generic",
 						ExceptionID:      2,
 						ParentID:         Pointer(1),
 						IsExceptionGroup: true,
diff --git a/logrus/README.md b/logrus/README.md
new file mode 100644
index 000000000..b4444530d
--- /dev/null
+++ logrus/README.md
@@ -0,0 +1,91 @@
+<p align="center">
+  <a href="https://sentry.io" target="_blank" align="center">
+    <img src="https://sentry-brand.storage.googleapis.com/sentry-logo-black.png" width="280">
+  </a>
+  <br />
+</p>
+
+# Official Sentry Logrus Hook for Sentry-go SDK
+
+**Go.dev Documentation:** https://pkg.go.dev/github.com/getsentry/sentry-go/logrus  
+**Example Usage:** https://github.com/getsentry/sentry-go/tree/master/_examples/logrus
+
+## Installation
+
+\`\`\`sh
+go get github.com/getsentry/sentry-go/logrus
+```
+
+## Usage
+
+```go
+import (
+	"fmt"
+	"os"
+	"time"
+
+	"github.com/sirupsen/logrus"
+	"github.com/getsentry/sentry-go"
+	sentrylogrus "github.com/getsentry/sentry-go/logrus"
+)
+
+func main() {
+	// Initialize Logrus
+	logger := logrus.New()
+	logger.Level = logrus.DebugLevel
+	logger.Out = os.Stderr
+
+	// Define log levels to send to Sentry
+	sentryLevels := []logrus.Level{logrus.ErrorLevel, logrus.FatalLevel, logrus.PanicLevel}
+
+	// Initialize Sentry
+	sentryHook, err := sentrylogrus.New(sentryLevels, sentry.ClientOptions{
+		Dsn: "your-public-dsn",
+		BeforeSend: func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event {
+			fmt.Println(event) // Optional debugging
+			return event
+		},
+		Debug: true,
+	})
+	if err != nil {
+		panic(err)
+	}
+	defer sentryHook.Flush(5 * time.Second)
+	logger.AddHook(sentryHook)
+
+	// Example logging
+	logger.WithField("user", "test-user").Error("An error occurred")
+
+	// Ensure all events are flushed before exit
+	logrus.RegisterExitHandler(func() { sentryHook.Flush(5 * time.Second) })
+}
+```
+
+## Configuration
+
+The `sentrylogrus` package accepts an array of `logrus.Level` and a struct of `sentry.ClientOptions` that allows you to configure how the hook will behave.
+The `logrus.Level` array defines which log levels should be sent to Sentry.
+
+In addition, the Hook returned by `sentrylogrus.New` can be configured with the following options:
+
+- Fallback Functionality: Configure a fallback for handling errors during log transmission.
+
+```go
+sentryHook.Fallback = func(entry *logrus.Entry, err error) {
+    // Handle error
+}
+```
+
+- Setting default tags for all events sent to Sentry
+
+```go
+sentryHook.AddTags(map[string]string{
+    "key": "value",
+})
+```
+- 
+
+## Notes
+
+- Always call Flush to ensure all events are sent to Sentry before program termination
+
diff --git logrus/logrusentry_test.go logrus/logrusentry_test.go
index db303e43c..de68e16d3 100644
--- logrus/logrusentry_test.go
+++ logrus/logrusentry_test.go
@@ -185,6 +185,7 @@ func Test_entryToEvent(t *testing.T) {
 						Mechanism: &sentry.Mechanism{
 							ExceptionID:      0,
 							IsExceptionGroup: true,
+							Type:             "generic",
 						},
 					},
 					{
@@ -197,6 +198,7 @@ func Test_entryToEvent(t *testing.T) {
 							ExceptionID:      1,
 							IsExceptionGroup: true,
 							ParentID:         sentry.Pointer(0),
+							Type:             "generic",
 						},
 					},
 				},
diff --git metrics.go metrics.go
index bcc94e88d..e6966bc3d 100644
--- metrics.go
+++ metrics.go
@@ -5,7 +5,7 @@ import (
 	"hash/crc32"
 	"math"
 	"regexp"
-	"sort"
+	"slices"
 	"strings"
 )
 
@@ -223,7 +223,7 @@ func (am abstractMetric) SerializeTags() string {
 	for k := range am.tags {
 		values = append(values, k)
 	}
-	sortSlice(values)
+	slices.Sort(values)
 
 	for _, key := range values {
 		val := sanitizeValue(am.tags[key])
@@ -375,7 +375,7 @@ func (s SetMetric[T]) SerializeValue() string {
 	for k := range s.values {
 		values = append(values, k)
 	}
-	sortSlice(values)
+	slices.Sort(values)
 
 	var sb strings.Builder
 	for _, el := range values {
@@ -419,9 +419,3 @@ func sanitizeValue(s string) string {
 type Ordered interface {
 	~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | ~float32 | ~float64 | ~string
 }
-
-func sortSlice[T Ordered](s []T) {
-	sort.Slice(s, func(i, j int) bool {
-		return s[i] < s[j]
-	})
-}
diff --git otel/context.go otel/context.go
index 1fd84c960..b4906eb0b 100644
--- otel/context.go
+++ otel/context.go
@@ -1,5 +1,3 @@
-//go:build go1.18
-
 package sentryotel
 
 // Context keys to be used with context.WithValue(...) and ctx.Value(...)
diff --git otel/event_processor.go otel/event_processor.go
index d171b7d6b..c86a63732 100644
--- otel/event_processor.go
+++ otel/event_processor.go
@@ -1,5 +1,3 @@
-//go:build go1.18
-
 package sentryotel
 
 import (
diff --git otel/event_processor_test.go otel/event_processor_test.go
index 692f462fd..43df64a9a 100644
--- otel/event_processor_test.go
+++ otel/event_processor_test.go
@@ -1,5 +1,3 @@
-//go:build go1.18
-
 package sentryotel
 
 import (
diff --git otel/go.mod otel/go.mod
index 17866e65d..135541291 100644
--- otel/go.mod
+++ otel/go.mod
@@ -1,9 +1,9 @@
 module github.com/getsentry/sentry-go/otel
 
-go 1.18
+go 1.21
 
 require (
-	github.com/getsentry/sentry-go v0.29.1
+	github.com/getsentry/sentry-go v0.30.0
 	github.com/google/go-cmp v0.5.9
 	go.opentelemetry.io/otel v1.11.0
 	go.opentelemetry.io/otel/sdk v1.11.0
diff --git otel/go.sum otel/go.sum
index 2f17982d0..6b9f2997e 100644
--- otel/go.sum
+++ otel/go.sum
@@ -1,5 +1,7 @@
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
+github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
 github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
 github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@@ -8,9 +10,13 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre
 github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
+github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
+github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
 go.opentelemetry.io/otel v1.11.0 h1:kfToEGMDq6TrVrJ9Vht84Y8y9enykSZzDDZglV0kIEk=
 go.opentelemetry.io/otel v1.11.0/go.mod h1:H2KtuEphyMvlhZ+F7tg9GRhAOe60moNx61Ex+WmiKkk=
 go.opentelemetry.io/otel/sdk v1.11.0 h1:ZnKIL9V9Ztaq+ME43IUi/eo22mNsb6a7tGfzaOWB5fo=
@@ -22,3 +28,4 @@ golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git otel/helpers_test.go otel/helpers_test.go
index e06274578..0ff6e0777 100644
--- otel/helpers_test.go
+++ otel/helpers_test.go
@@ -1,5 +1,3 @@
-//go:build go1.18
-
 package sentryotel
 
 import (
diff --git otel/internal/utils/mapotelstatus.go otel/internal/utils/mapotelstatus.go
index 2d4614869..294e1536f 100644
--- otel/internal/utils/mapotelstatus.go
+++ otel/internal/utils/mapotelstatus.go
@@ -1,5 +1,3 @@
-//go:build go1.18
-
 package utils
 
 import (
diff --git otel/internal/utils/sentryrequest.go otel/internal/utils/sentryrequest.go
index 8b078d5ee..3a117f6f0 100644
--- otel/internal/utils/sentryrequest.go
+++ otel/internal/utils/sentryrequest.go
@@ -1,5 +1,3 @@
-//go:build go1.18
-
 package utils
 
 import (
diff --git otel/internal/utils/spanattributes.go otel/internal/utils/spanattributes.go
index 0571d23d3..65e3c25ae 100644
--- otel/internal/utils/spanattributes.go
+++ otel/internal/utils/spanattributes.go
@@ -1,5 +1,3 @@
-//go:build go1.18
-
 package utils
 
 import (
diff --git otel/propagator.go otel/propagator.go
index 8a5d3e626..8f17ff4fe 100644
--- otel/propagator.go
+++ otel/propagator.go
@@ -1,5 +1,3 @@
-//go:build go1.18
-
 package sentryotel
 
 import (
diff --git otel/propagator_test.go otel/propagator_test.go
index 0941aa3bd..d4d537941 100644
--- otel/propagator_test.go
+++ otel/propagator_test.go
@@ -1,5 +1,3 @@
-//go:build go1.18
-
 package sentryotel
 
 import (
diff --git otel/span_map.go otel/span_map.go
index 9390a8a74..5340ff30a 100644
--- otel/span_map.go
+++ otel/span_map.go
@@ -1,5 +1,3 @@
-//go:build go1.18
-
 package sentryotel
 
 import (
diff --git otel/span_processor.go otel/span_processor.go
index 2f59c0a20..263d77280 100644
--- otel/span_processor.go
+++ otel/span_processor.go
@@ -1,5 +1,3 @@
-//go:build go1.18
-
 package sentryotel
 
 import (
diff --git otel/span_processor_test.go otel/span_processor_test.go
index 6dbda27a6..03c9796cd 100644
--- otel/span_processor_test.go
+++ otel/span_processor_test.go
@@ -1,5 +1,3 @@
-//go:build go1.18
-
 package sentryotel
 
 import (
diff --git profiler.go profiler.go
index c0b858cc1..c4f98dfeb 100644
--- profiler.go
+++ profiler.go
@@ -2,6 +2,7 @@ package sentry
 
 import (
 	"container/ring"
+	"encoding/binary"
 	"strconv"
 
 	"runtime"
@@ -377,13 +378,7 @@ func (p *profileRecorder) addStackTrace(capturedStack traceparser.Trace) int {
 
 			p.stackKeyBuffer = append(p.stackKeyBuffer, 0) // space
 
-			// The following code is just like binary.AppendUvarint() which isn't yet available in Go 1.18.
-			x := uint64(frameIndex) + 1
-			for x >= 0x80 {
-				p.stackKeyBuffer = append(p.stackKeyBuffer, byte(x)|0x80)
-				x >>= 7
-			}
-			p.stackKeyBuffer = append(p.stackKeyBuffer, byte(x))
+			p.stackKeyBuffer = binary.AppendUvarint(p.stackKeyBuffer, uint64(frameIndex)+1)
 		}
 	}
 
diff --git scope.go scope.go
index 5be1c02e1..8245fdbd2 100644
--- scope.go
+++ scope.go
@@ -478,7 +478,7 @@ func (scope *Scope) ApplyToEvent(event *Event, hint *EventHint, client *Client)
 // a proper deep copy: if some context values are pointer types (e.g. maps),
 // they won't be properly copied.
 func cloneContext(c Context) Context {
-	res := Context{}
+	res := make(Context, len(c))
 	for k, v := range c {
 		res[k] = v
 	}
diff --git sentry.go sentry.go
index 476eb178d..49c172318 100644
--- sentry.go
+++ sentry.go
@@ -6,7 +6,7 @@ import (
 )
 
 // The version of the SDK.
-const SDKVersion = "0.29.1"
+const SDKVersion = "0.30.0"
 
 // apiVersion is the minimum version of the Sentry API compatible with the
 // sentry-go SDK.
diff --git a/slog/README.MD b/slog/README.MD
new file mode 100644
index 000000000..494fda8ad
--- /dev/null
+++ slog/README.MD
@@ -0,0 +1,93 @@
+<p align="center">
+  <a href="https://sentry.io" target="_blank" align="center">
+    <img src="https://sentry-brand.storage.googleapis.com/sentry-logo-black.png" width="280">
+  </a>
+  <br />
+</p>
+
+# Official Sentry Integration for slog
+
+**Go.dev Documentation:** https://pkg.go.dev/github.com/getsentry/sentry-go/slog
+**Example Usage:** https://github.com/getsentry/sentry-go/tree/master/_examples/slog
+
+---
+
+## Installation
+
+```sh
+go get github.com/getsentry/sentry-go/slog
+
+```
+
+## Usage
+
+```go
+package main
+
+import (
+	"context"
+	"log/slog"
+
+	"github.com/getsentry/sentry-go"
+	slogSentry "github.com/getsentry/sentry-go/slog"
+)
+
+func main() {
+	// Initialize Sentry
+	err := sentry.Init(sentry.ClientOptions{
+		Dsn: "your-public-dsn",
+		Debug: true,
+	})
+	if err != nil {
+		panic(err)
+	}
+	defer sentry.Flush(5 * time.Second)
+
+	// Set up slog with Sentry handler
+	handler := slogSentry.Option{
+		Level: slog.LevelError, // Minimum log level
+		AddSource: true,        // Include file/line source info
+	}.NewSentryHandler()
+	logger := slog.New(handler)
+
+	// Example logging
+	logger.Info("This will not be sent to Sentry")
+	logger.Error("An error occurred", "user", "test-user")
+}
+```
+
+## Configuration
+
+The slog-sentry package offers several options to customize how logs are handled and sent to Sentry. These are specified through the Option struct:
+
+- `Level`: Minimum log level to send to Sentry. Defaults to `slog.LevelDebug`.
+
+- `Hub`: Custom Sentry hub to use; defaults to the current Sentry hub if not set.
+
+- `Converter`: Custom function to transform logs into Sentry events (default is DefaultConverter).
+
+- `AttrFromContext`: Functions to extract additional attributes from the context.
+
+- `AddSource`: Include file/line source info in Sentry events. Defaults to `false`.
+
+- `ReplaceAttr`:  Allows modification or filtering of attributes before sending to Sentry.
+
+
+### Example Customization
+
+```go
+handler := slogSentry.Option{
+	Level: slog.LevelWarn,
+	Converter: func(addSource bool, replaceAttr func([]string, slog.Attr) slog.Attr, attrs []slog.Attr, groups []string, record *slog.Record, hub *sentry.Hub) *sentry.Event {
+		// Custom conversion logic
+		return &sentry.Event{
+			Message: record.Message,
+		}
+	},
+	AddSource: true,
+}.NewSentryHandler()
+```
+
+## Notes
+
+- Always call Flush to ensure all events are sent to Sentry before program termination
diff --git a/slog/common.go b/slog/common.go
new file mode 100644
index 000000000..8fd63a344
--- /dev/null
+++ slog/common.go
@@ -0,0 +1,260 @@
+package sentryslog
+
+import (
+	"context"
+	"encoding"
+	"fmt"
+	"log/slog"
+	"runtime"
+	"strconv"
+)
+
+func source(sourceKey string, r *slog.Record) slog.Attr {
+	fs := runtime.CallersFrames([]uintptr{r.PC})
+	f, _ := fs.Next()
+	var args []any
+	if f.Function != "" {
+		args = append(args, slog.String("function", f.Function))
+	}
+	if f.File != "" {
+		args = append(args, slog.String("file", f.File))
+	}
+	if f.Line != 0 {
+		args = append(args, slog.Int("line", f.Line))
+	}
+
+	return slog.Group(sourceKey, args...)
+}
+
+type replaceAttrFn = func(groups []string, a slog.Attr) slog.Attr
+
+func replaceAttrs(fn replaceAttrFn, groups []string, attrs ...slog.Attr) []slog.Attr {
+	for i := range attrs {
+		attr := attrs[i]
+		value := attr.Value.Resolve()
+		if value.Kind() == slog.KindGroup {
+			attrs[i].Value = slog.GroupValue(replaceAttrs(fn, append(groups, attr.Key), value.Group()...)...)
+		} else if fn != nil {
+			attrs[i] = fn(groups, attr)
+		}
+	}
+
+	return attrs
+}
+
+func attrsToMap(attrs ...slog.Attr) map[string]any {
+	output := make(map[string]any, len(attrs))
+
+	attrsByKey := groupValuesByKey(attrs)
+	for k, values := range attrsByKey {
+		v := mergeAttrValues(values...)
+		if v.Kind() == slog.KindGroup {
+			output[k] = attrsToMap(v.Group()...)
+		} else {
+			output[k] = v.Any()
+		}
+	}
+
+	return output
+}
+
+func extractError(attrs []slog.Attr) ([]slog.Attr, error) {
+	for i := range attrs {
+		attr := attrs[i]
+
+		if _, ok := errorKeys[attr.Key]; !ok {
+			continue
+		}
+
+		if err, ok := attr.Value.Resolve().Any().(error); ok {
+			return append(attrs[:i], attrs[i+1:]...), err
+		}
+	}
+
+	return attrs, nil
+}
+
+func mergeAttrValues(values ...slog.Value) slog.Value {
+	v := values[0]
+
+	for i := 1; i < len(values); i++ {
+		if v.Kind() != slog.KindGroup || values[i].Kind() != slog.KindGroup {
+			v = values[i]
+			continue
+		}
+
+		v = slog.GroupValue(append(v.Group(), values[i].Group()...)...)
+	}
+
+	return v
+}
+
+func groupValuesByKey(attrs []slog.Attr) map[string][]slog.Value {
+	result := make(map[string][]slog.Value)
+
+	for _, item := range attrs {
+		key := item.Key
+		result[key] = append(result[key], item.Value)
+	}
+
+	return result
+}
+
+func attrsToString(attrs ...slog.Attr) map[string]string {
+	output := make(map[string]string, len(attrs))
+
+	for _, attr := range attrs {
+		k, v := attr.Key, attr.Value
+		output[k] = valueToString(v)
+	}
+
+	return output
+}
+
+func valueToString(v slog.Value) string {
+	switch v.Kind() {
+	case slog.KindAny, slog.KindLogValuer, slog.KindGroup:
+		return anyValueToString(v)
+	case slog.KindInt64:
+		return fmt.Sprintf("%d", v.Int64())
+	case slog.KindUint64:
+		return fmt.Sprintf("%d", v.Uint64())
+	case slog.KindFloat64:
+		return fmt.Sprintf("%f", v.Float64())
+	case slog.KindString:
+		return v.String()
+	case slog.KindBool:
+		return strconv.FormatBool(v.Bool())
+	case slog.KindDuration:
+		return v.Duration().String()
+	case slog.KindTime:
+		return v.Time().UTC().String()
+	}
+	return anyValueToString(v)
+}
+
+func anyValueToString(v slog.Value) string {
+	tm, ok := v.Any().(encoding.TextMarshaler)
+	if !ok {
+		return fmt.Sprintf("%+v", v.Any())
+	}
+
+	data, err := tm.MarshalText()
+	if err != nil {
+		return fmt.Sprintf("%+v", v.Any())
+	}
+
+	return string(data)
+}
+
+func appendRecordAttrsToAttrs(attrs []slog.Attr, groups []string, record *slog.Record) []slog.Attr {
+	output := make([]slog.Attr, len(attrs))
+	copy(output, attrs)
+
+	for i, j := 0, len(groups)-1; i < j; i, j = i+1, j-1 {
+		groups[i], groups[j] = groups[j], groups[i]
+	}
+	record.Attrs(func(attr slog.Attr) bool {
+		for i := range groups {
+			attr = slog.Group(groups[i], attr)
+		}
+		output = append(output, attr)
+		return true
+	})
+
+	return output
+}
+
+func removeEmptyAttrs(attrs []slog.Attr) []slog.Attr {
+	result := []slog.Attr{}
+
+	for _, attr := range attrs {
+		if attr.Key == "" {
+			continue
+		}
+
+		if attr.Value.Kind() == slog.KindGroup {
+			values := removeEmptyAttrs(attr.Value.Group())
+			if len(values) == 0 {
+				continue
+			}
+			attr.Value = slog.GroupValue(values...)
+			result = append(result, attr)
+		} else if !attr.Value.Equal(slog.Value{}) {
+			result = append(result, attr)
+		}
+	}
+
+	return result
+}
+
+func contextExtractor(ctx context.Context, fns []func(ctx context.Context) []slog.Attr) []slog.Attr {
+	attrs := []slog.Attr{}
+	for _, fn := range fns {
+		attrs = append(attrs, fn(ctx)...)
+	}
+	return attrs
+}
+
+func appendAttrsToGroup(groups []string, actualAttrs []slog.Attr, newAttrs ...slog.Attr) []slog.Attr {
+	actualAttrsCopy := make([]slog.Attr, len(actualAttrs))
+	copy(actualAttrsCopy, actualAttrs)
+
+	if len(groups) == 0 {
+		return uniqAttrs(append(actualAttrsCopy, newAttrs...))
+	}
+
+	groupKey := groups[0]
+	for i := range actualAttrsCopy {
+		attr := actualAttrsCopy[i]
+		if attr.Key == groupKey && attr.Value.Kind() == slog.KindGroup {
+			actualAttrsCopy[i] = slog.Group(groupKey, toAnySlice(appendAttrsToGroup(groups[1:], attr.Value.Group(), newAttrs...))...)
+			return actualAttrsCopy
+		}
+	}
+
+	return uniqAttrs(
+		append(
+			actualAttrsCopy,
+			slog.Group(
+				groupKey,
+				toAnySlice(appendAttrsToGroup(groups[1:], []slog.Attr{}, newAttrs...))...,
+			),
+		),
+	)
+}
+
+func toAnySlice(collection []slog.Attr) []any {
+	result := make([]any, len(collection))
+	for i := range collection {
+		result[i] = collection[i]
+	}
+	return result
+}
+
+func uniqAttrs(attrs []slog.Attr) []slog.Attr {
+	return uniqByLast(attrs, func(item slog.Attr) string {
+		return item.Key
+	})
+}
+
+func uniqByLast[T any, U comparable](collection []T, iteratee func(item T) U) []T {
+	result := make([]T, 0, len(collection))
+	seen := make(map[U]int, len(collection))
+	seenIndex := 0
+
+	for _, item := range collection {
+		key := iteratee(item)
+
+		if index, ok := seen[key]; ok {
+			result[index] = item
+			continue
+		}
+
+		seen[key] = seenIndex
+		seenIndex++
+		result = append(result, item)
+	}
+
+	return result
+}
diff --git a/slog/common_test.go b/slog/common_test.go
new file mode 100644
index 000000000..1713965c5
--- /dev/null
+++ slog/common_test.go
@@ -0,0 +1,569 @@
+package sentryslog
+
+import (
+	"context"
+	"log/slog"
+	"runtime"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestSource(t *testing.T) {
+	// Simulate a runtime frame
+	pc, file, _, _ := runtime.Caller(0)
+	record := &slog.Record{PC: pc}
+
+	// Call the source function
+	attr := source("sourceKey", record)
+
+	// Assert the attributes
+	assert.Equal(t, "sourceKey", attr.Key)
+	assert.Equal(t, slog.KindGroup, attr.Value.Kind())
+
+	groupAttrs := attr.Value.Group()
+
+	expectedAttrs := map[string]any{
+		"function": "github.com/getsentry/sentry-go/slog.TestSource",
+		"file":     file,
+		"line":     int64(15),
+	}
+
+	for _, a := range groupAttrs {
+		expectedValue, ok := expectedAttrs[a.Key]
+		if assert.True(t, ok, "unexpected attribute key: %s", a.Key) {
+			assert.Equal(t, expectedValue, a.Value.Any())
+		}
+	}
+}
+
+type testLogValuer struct {
+	name string
+	pass string
+}
+
+func (t testLogValuer) LogValue() slog.Value {
+	return slog.GroupValue(
+		slog.String("name", t.name),
+		slog.String("password", "********"),
+	)
+}
+
+var stubLogValuer = testLogValuer{"userName", "password"}
+
+func TestReplaceAttrs(t *testing.T) {
+	t.Parallel()
+	is := assert.New(t)
+
+	// no ReplaceAttr func
+	is.Equal(
+		[]slog.Attr{slog.Bool("bool", true), slog.Int("int", 42)},
+		replaceAttrs(
+			nil,
+			[]string{"foobar"},
+			slog.Bool("bool", true), slog.Int("int", 42),
+		),
+	)
+
+	// no ReplaceAttr func, but convert struct with interface slog.LogValue in slog.Group
+	is.Equal(
+		[]slog.Attr{slog.Group("user", slog.String("name", stubLogValuer.name), slog.String("password", "********"))},
+		replaceAttrs(
+			nil,
+			[]string{"foobar"},
+			slog.Any("user", stubLogValuer),
+		),
+	)
+
+	// ReplaceAttr func, but convert struct with interface slog.LogValue in slog.Group
+	is.Equal(
+		[]slog.Attr{slog.Group("user", slog.String("name", stubLogValuer.name), slog.String("password", "********"))},
+		replaceAttrs(
+			func(groups []string, a slog.Attr) slog.Attr {
+				is.Equal([]string{"foobar", "user"}, groups)
+				return a
+			},
+			[]string{"foobar"},
+			slog.Any("user", stubLogValuer),
+		),
+	)
+
+	// ReplaceAttr func, but returns the same attributes
+	is.Equal(
+		[]slog.Attr{slog.Bool("bool", true), slog.Int("int", 42)},
+		replaceAttrs(
+			func(groups []string, a slog.Attr) slog.Attr {
+				is.Equal("foobar", groups[0])
+				return a
+			},
+			[]string{"foobar"},
+			slog.Bool("bool", true), slog.Int("int", 42),
+		),
+	)
+
+	// Replace int and divide by 2
+	is.Equal(
+		[]slog.Attr{slog.Bool("bool", true), slog.Int("int", 21)},
+		replaceAttrs(
+			func(groups []string, a slog.Attr) slog.Attr {
+				is.Equal("foobar", groups[0])
+				if a.Value.Kind() == slog.KindInt64 {
+					a.Value = slog.Int64Value(a.Value.Int64() / 2)
+				}
+				return a
+			},
+			[]string{"foobar"},
+			slog.Bool("bool", true), slog.Int("int", 42),
+		),
+	)
+
+	// Remove int attr
+	is.Equal(
+		[]slog.Attr{slog.Bool("bool", true), slog.Any("int", nil)},
+		replaceAttrs(
+			func(groups []string, a slog.Attr) slog.Attr {
+				is.Equal("foobar", groups[0])
+				if a.Value.Kind() == slog.KindInt64 {
+					return slog.Any("int", nil)
+				}
+				return a
+			},
+			[]string{"foobar"},
+			slog.Bool("bool", true), slog.Int("int", 42),
+		),
+	)
+
+	// Rename int attr
+	is.Equal(
+		[]slog.Attr{slog.Bool("bool", true), slog.Int("int2", 21)},
+		replaceAttrs(
+			func(groups []string, a slog.Attr) slog.Attr {
+				is.Equal("foobar", groups[0])
+				if a.Value.Kind() == slog.KindInt64 {
+					return slog.Int("int2", 21)
+				}
+				return a
+			},
+			[]string{"foobar"},
+			slog.Bool("bool", true), slog.Int("int", 42),
+		),
+	)
+
+	// Rename attr in groups
+	is.Equal(
+		[]slog.Attr{slog.Bool("bool", true), slog.Group("group1", slog.Group("group2", slog.Int("int", 21)))},
+		replaceAttrs(
+			func(groups []string, a slog.Attr) slog.Attr {
+				is.Equal("foobar", groups[0])
+				if len(groups) > 1 {
+					is.Equal([]string{"foobar", "group1", "group2"}, groups)
+					return slog.Int("int", 21)
+				}
+				return a
+			},
+			[]string{"foobar"},
+			slog.Bool("bool", true), slog.Group("group1", slog.Group("group2", slog.String("string", "foobar"))),
+		),
+	)
+}
+
+func TestAttrsToMap(t *testing.T) {
+	t.Parallel()
+	is := assert.New(t)
+
+	// simple
+	is.EqualValues(
+		map[string]any{"key": "value"},
+		attrsToMap(slog.Any("key", "value")),
+	)
+
+	// nested
+	is.EqualValues(
+		map[string]any{"key": "value", "key1": map[string]any{"key2": "value2"}},
+		attrsToMap(slog.Any("key", "value"), slog.Group("key1", slog.Any("key2", "value2"))),
+	)
+
+	// merge
+	is.EqualValues(
+		map[string]any{"key": "value", "key1": map[string]any{"key2": "value2", "key3": "value3"}},
+		attrsToMap(
+			slog.Any("key", "value"),
+			slog.Group("key1", slog.Any("key2", "value2")),
+			slog.Group("key1", slog.Any("key3", "value3")),
+		),
+	)
+}
+
+func TestExtractError(t *testing.T) {
+	t.Parallel()
+	is := assert.New(t)
+
+	// not found
+	attrs, err := extractError(
+		[]slog.Attr{
+			slog.Any("key", "value"),
+			slog.Group("key1", slog.Any("key2", "value2")),
+			slog.String("foo", "bar"),
+		},
+	)
+	is.Len(attrs, 3)
+	is.Nil(err)
+
+	// found key but wrong type
+	attrs, err = extractError(
+		[]slog.Attr{
+			slog.Any("key", "value"),
+			slog.Group("key1", slog.Any("key2", "value2")),
+			slog.String("error", "bar"),
+		},
+	)
+	is.Len(attrs, 3)
+	is.Nil(err)
+
+	// found start first key
+	attrs, err = extractError(
+		[]slog.Attr{
+			slog.Any("error", assert.AnError),
+			slog.Any("key", "value"),
+			slog.Group("key1", slog.Any("key2", "value2")),
+			slog.String("foo", "bar"),
+		},
+	)
+	is.Len(attrs, 3)
+	is.EqualError(err, assert.AnError.Error())
+
+	// found start second key
+	attrs, err = extractError(
+		[]slog.Attr{
+			slog.Any("err", assert.AnError),
+			slog.Any("key", "value"),
+			slog.Group("key1", slog.Any("key2", "value2")),
+			slog.String("foo", "bar"),
+		},
+	)
+	is.Len(attrs, 3)
+	is.EqualError(err, assert.AnError.Error())
+
+	// found middle
+	attrs, err = extractError(
+		[]slog.Attr{
+			slog.Any("key", "value"),
+			slog.Any("error", assert.AnError),
+			slog.Group("key1", slog.Any("key2", "value2")),
+			slog.String("foo", "bar"),
+		},
+	)
+	is.Len(attrs, 3)
+	is.EqualError(err, assert.AnError.Error())
+
+	// found end
+	attrs, err = extractError(
+		[]slog.Attr{
+			slog.Any("key", "value"),
+			slog.Group("key1", slog.Any("key2", "value2")),
+			slog.String("foo", "bar"),
+			slog.Any("error", assert.AnError),
+		},
+	)
+	is.Len(attrs, 3)
+	is.EqualError(err, assert.AnError.Error())
+}
+
+func TestRemoveEmptyAttrs(t *testing.T) {
+	t.Parallel()
+	is := assert.New(t)
+
+	// do not remove anything
+	is.Equal(
+		[]slog.Attr{slog.Bool("bool", true), slog.Int("int", 42)},
+		removeEmptyAttrs(
+			[]slog.Attr{slog.Bool("bool", true), slog.Int("int", 42)},
+		),
+	)
+	is.Equal(
+		[]slog.Attr{slog.Bool("bool", false), slog.Int("int", 42)},
+		removeEmptyAttrs(
+			[]slog.Attr{slog.Bool("bool", false), slog.Int("int", 42)},
+		),
+	)
+
+	// remove if missing keys
+	is.Equal(
+		[]slog.Attr{slog.Int("int", 42)},
+		removeEmptyAttrs(
+			[]slog.Attr{slog.Bool("", true), slog.Int("int", 42)},
+		),
+	)
+
+	// remove if missing value
+	is.Equal(
+		[]slog.Attr{slog.Int("int", 42)},
+		removeEmptyAttrs(
+			[]slog.Attr{slog.Any("test", nil), slog.Int("int", 42)},
+		),
+	)
+	is.Equal(
+		[]slog.Attr{slog.Int("int", 42)},
+		removeEmptyAttrs(
+			[]slog.Attr{slog.Group("test"), slog.Int("int", 42)},
+		),
+	)
+
+	// remove nested
+	is.Equal(
+		[]slog.Attr{slog.Int("int", 42)},
+		removeEmptyAttrs(
+			[]slog.Attr{slog.Any("test", nil), slog.Int("int", 42)},
+		),
+	)
+	is.Equal(
+		[]slog.Attr{slog.Int("int", 42)},
+		removeEmptyAttrs(
+			[]slog.Attr{slog.Group("test", slog.Any("foobar", nil)), slog.Int("int", 42)},
+		),
+	)
+}
+
+func TestValueToString(t *testing.T) {
+	tests := map[string]struct {
+		input    slog.Attr
+		expected string
+	}{
+		"KindInt64": {
+			input:    slog.Int64("key", 42),
+			expected: "42",
+		},
+		"KindUint64": {
+			input:    slog.Uint64("key", 42),
+			expected: "42",
+		},
+		"KindFloat64": {
+			input:    slog.Float64("key", 3.14),
+			expected: "3.140000",
+		},
+		"KindString": {
+			input:    slog.String("key", "test"),
+			expected: "test",
+		},
+		"KindBool": {
+			input:    slog.Bool("key", true),
+			expected: "true",
+		},
+		"KindDuration": {
+			input:    slog.Duration("key", time.Second*42),
+			expected: "42s",
+		},
+		"KindTime": {
+			input:    slog.Time("key", time.Date(2023, time.July, 30, 12, 0, 0, 0, time.UTC)),
+			expected: "2023-07-30 12:00:00 +0000 UTC",
+		},
+		"KindAny": {
+			input:    slog.Any("key", "any value"),
+			expected: "any value",
+		},
+	}
+
+	for name, tc := range tests {
+		t.Run(name, func(t *testing.T) {
+			actual := valueToString(tc.input.Value)
+			assert.Equal(t, tc.expected, actual)
+		})
+	}
+}
+
+type ctxKey string
+
+func TestContextExtractor(t *testing.T) {
+	tests := map[string]struct {
+		ctx      context.Context
+		fns      []func(ctx context.Context) []slog.Attr
+		expected []slog.Attr
+	}{
+		"NoFunctions": {
+			ctx:      context.Background(),
+			fns:      []func(ctx context.Context) []slog.Attr{},
+			expected: []slog.Attr{},
+		},
+		"SingleFunction": {
+			ctx: context.Background(),
+			fns: []func(ctx context.Context) []slog.Attr{
+				func(ctx context.Context) []slog.Attr {
+					return []slog.Attr{slog.String("key1", "value1")}
+				},
+			},
+			expected: []slog.Attr{slog.String("key1", "value1")},
+		},
+		"MultipleFunctions": {
+			ctx: context.Background(),
+			fns: []func(ctx context.Context) []slog.Attr{
+				func(ctx context.Context) []slog.Attr {
+					return []slog.Attr{slog.String("key1", "value1")}
+				},
+				func(ctx context.Context) []slog.Attr {
+					return []slog.Attr{slog.String("key2", "value2")}
+				},
+			},
+			expected: []slog.Attr{slog.String("key1", "value1"), slog.String("key2", "value2")},
+		},
+		"FunctionWithContext": {
+			ctx: context.WithValue(context.Background(), ctxKey("userID"), "1234"),
+			fns: []func(ctx context.Context) []slog.Attr{
+				func(ctx context.Context) []slog.Attr {
+					if userID, ok := ctx.Value(ctxKey("userID")).(string); ok {
+						return []slog.Attr{slog.String("userID", userID)}
+					}
+					return []slog.Attr{}
+				},
+			},
+			expected: []slog.Attr{slog.String("userID", "1234")},
+		},
+	}
+
+	for name, tc := range tests {
+		t.Run(name, func(t *testing.T) {
+			actual := contextExtractor(tc.ctx, tc.fns)
+			assert.Equal(t, tc.expected, actual)
+		})
+	}
+}
+
+func TestAppendAttrsToGroup(t *testing.T) {
+	tests := map[string]struct {
+		groups      []string
+		actualAttrs []slog.Attr
+		newAttrs    []slog.Attr
+		expected    []slog.Attr
+	}{
+		"NoGroups": {
+			groups:      []string{},
+			actualAttrs: []slog.Attr{slog.String("key1", "value1")},
+			newAttrs:    []slog.Attr{slog.String("key2", "value2")},
+			expected:    []slog.Attr{slog.String("key1", "value1"), slog.String("key2", "value2")},
+		},
+		"SingleGroup": {
+			groups: []string{"group1"},
+			actualAttrs: []slog.Attr{
+				slog.Group("group1", slog.String("key1", "value1")),
+			},
+			newAttrs: []slog.Attr{slog.String("key2", "value2")},
+			expected: []slog.Attr{
+				slog.Group("group1", slog.String("key1", "value1"), slog.String("key2", "value2")),
+			},
+		},
+		"NestedGroups": {
+			groups: []string{"group1", "group2"},
+			actualAttrs: []slog.Attr{
+				slog.Group("group1", slog.Group("group2", slog.String("key1", "value1"))),
+			},
+			newAttrs: []slog.Attr{slog.String("key2", "value2")},
+			expected: []slog.Attr{
+				slog.Group("group1", slog.Group("group2", slog.String("key1", "value1"), slog.String("key2", "value2"))),
+			},
+		},
+		"NewGroup": {
+			groups:      []string{"group1"},
+			actualAttrs: []slog.Attr{slog.String("key1", "value1")},
+			newAttrs:    []slog.Attr{slog.String("key2", "value2")},
+			expected: []slog.Attr{
+				slog.String("key1", "value1"),
+				slog.Group("group1", slog.String("key2", "value2")),
+			},
+		},
+	}
+
+	for name, tc := range tests {
+		t.Run(name, func(t *testing.T) {
+			actual := appendAttrsToGroup(tc.groups, tc.actualAttrs, tc.newAttrs...)
+			assert.Equal(t, tc.expected, actual)
+		})
+	}
+}
+
+func TestToAnySlice(t *testing.T) {
+	tests := map[string]struct {
+		input    []slog.Attr
+		expected []any
+	}{
+		"EmptySlice": {
+			input:    []slog.Attr{},
+			expected: []any{},
+		},
+		"SingleElement": {
+			input:    []slog.Attr{slog.String("key1", "value1")},
+			expected: []any{slog.String("key1", "value1")},
+		},
+		"MultipleElements": {
+			input: []slog.Attr{
+				slog.String("key1", "value1"),
+				slog.Int("key2", 2),
+				slog.Bool("key3", true),
+			},
+			expected: []any{
+				slog.String("key1", "value1"),
+				slog.Int("key2", 2),
+				slog.Bool("key3", true),
+			},
+		},
+	}
+
+	for name, tc := range tests {
+		t.Run(name, func(t *testing.T) {
+			actual := toAnySlice(tc.input)
+			assert.Equal(t, tc.expected, actual)
+		})
+	}
+}
+
+type textMarshalerExample struct {
+	Data string
+}
+
+func (t textMarshalerExample) MarshalText() (text []byte, err error) {
+	return []byte(t.Data), nil
+}
+
+type nonTextMarshalerExample struct {
+	Data string
+}
+
+func TestAnyValueToString(t *testing.T) {
+	tests := map[string]struct {
+		input    slog.Attr
+		expected string
+	}{
+		"TextMarshaler implementation": {
+			input:    slog.Any("key", textMarshalerExample{Data: "example"}),
+			expected: "example",
+		},
+		"Non-TextMarshaler implementation": {
+			input:    slog.Any("key", nonTextMarshalerExample{Data: "example"}),
+			expected: "{Data:example}",
+		},
+		"String value": {
+			input:    slog.String("key", "example string"),
+			expected: "example string",
+		},
+		"Integer value": {
+			input:    slog.Int("key", 42),
+			expected: "42",
+		},
+		"Boolean value": {
+			input:    slog.Bool("key", true),
+			expected: "true",
+		},
+		"Nil value": {
+			input:    slog.Any("key", nil),
+			expected: "<nil>",
+		},
+	}
+
+	for name, tt := range tests {
+		t.Run(name, func(t *testing.T) {
+			output := anyValueToString(tt.input.Value)
+			if output != tt.expected {
+				t.Errorf("expected %s, got %s", tt.expected, output)
+			}
+		})
+	}
+}
diff --git a/slog/converter.go b/slog/converter.go
new file mode 100644
index 000000000..0e2fcd3e3
--- /dev/null
+++ slog/converter.go
@@ -0,0 +1,127 @@
+package sentryslog
+
+import (
+	"encoding"
+	"fmt"
+	"log/slog"
+	"net/http"
+
+	"github.com/getsentry/sentry-go"
+)
+
+var (
+	sourceKey = "source"
+	errorKeys = map[string]struct{}{
+		"error": {},
+		"err":   {},
+	}
+	name = "slog"
+)
+
+type Converter func(addSource bool, replaceAttr func(groups []string, a slog.Attr) slog.Attr, loggerAttr []slog.Attr, groups []string, record *slog.Record, hub *sentry.Hub) *sentry.Event
+
+func DefaultConverter(addSource bool, replaceAttr func(groups []string, a slog.Attr) slog.Attr, loggerAttr []slog.Attr, groups []string, record *slog.Record, _ *sentry.Hub) *sentry.Event {
+	// aggregate all attributes
+	attrs := appendRecordAttrsToAttrs(loggerAttr, groups, record)
+
+	// developer formatters
+	if addSource {
+		attrs = append(attrs, source(sourceKey, record))
+	}
+	attrs = replaceAttrs(replaceAttr, []string{}, attrs...)
+	attrs = removeEmptyAttrs(attrs)
+	attrs, err := extractError(attrs)
+
+	// handler formatter
+	event := sentry.NewEvent()
+	event.Timestamp = record.Time.UTC()
+	event.Level = LogLevels[record.Level]
+	event.Message = record.Message
+	event.Logger = name
+	event.SetException(err, 10)
+
+	for i := range attrs {
+		attrToSentryEvent(attrs[i], event)
+	}
+
+	return event
+}
+
+func attrToSentryEvent(attr slog.Attr, event *sentry.Event) {
+	k := attr.Key
+	v := attr.Value
+	kind := v.Kind()
+
+	switch {
+	case k == "dist" && kind == slog.KindString:
+		event.Dist = v.String()
+	case k == "environment" && kind == slog.KindString:
+		event.Environment = v.String()
+	case k == "event_id" && kind == slog.KindString:
+		event.EventID = sentry.EventID(v.String())
+	case k == "platform" && kind == slog.KindString:
+		event.Platform = v.String()
+	case k == "release" && kind == slog.KindString:
+		event.Release = v.String()
+	case k == "server_name" && kind == slog.KindString:
+		event.ServerName = v.String()
+	case k == "tags" && kind == slog.KindGroup:
+		event.Tags = attrsToString(v.Group()...)
+	case k == "transaction" && kind == slog.KindString:
+		event.Transaction = v.String()
+	case k == "user" && kind == slog.KindGroup:
+		handleUserAttributes(v, event)
+	case k == "request" && kind == slog.KindAny:
+		handleRequestAttributes(v, event)
+	case kind == slog.KindGroup:
+		event.Extra[k] = attrsToMap(v.Group()...)
+	default:
+		event.Extra[k] = v.Any()
+	}
+}
+
+func handleUserAttributes(v slog.Value, event *sentry.Event) {
+	data := attrsToString(v.Group()...)
+	if id, ok := data["id"]; ok {
+		event.User.ID = id
+		delete(data, "id")
+	}
+	if email, ok := data["email"]; ok {
+		event.User.Email = email
+		delete(data, "email")
+	}
+	if ipAddress, ok := data["ip_address"]; ok {
+		event.User.IPAddress = ipAddress
+		delete(data, "ip_address")
+	}
+	if username, ok := data["username"]; ok {
+		event.User.Username = username
+		delete(data, "username")
+	}
+	if name, ok := data["name"]; ok {
+		event.User.Name = name
+		delete(data, "name")
+	}
+	if segment, ok := data["segment"]; ok {
+		event.User.Segment = segment
+		delete(data, "segment")
+	}
+	event.User.Data = data
+}
+
+func handleRequestAttributes(v slog.Value, event *sentry.Event) {
+	if req, ok := v.Any().(http.Request); ok {
+		event.Request = sentry.NewRequest(&req)
+	} else if req, ok := v.Any().(*http.Request); ok {
+		event.Request = sentry.NewRequest(req)
+	} else {
+		if tm, ok := v.Any().(encoding.TextMarshaler); ok {
+			data, err := tm.MarshalText()
+			if err == nil {
+				event.User.Data["request"] = string(data)
+			} else {
+				event.User.Data["request"] = fmt.Sprintf("%v", v.Any())
+			}
+		}
+	}
+}
diff --git a/slog/converter_test.go b/slog/converter_test.go
new file mode 100644
index 000000000..746ac714c
--- /dev/null
+++ slog/converter_test.go
@@ -0,0 +1,188 @@
+package sentryslog
+
+import (
+	"log/slog"
+	"net/http"
+	"net/url"
+	"testing"
+	"time"
+
+	"github.com/getsentry/sentry-go"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestDefaultConverter(t *testing.T) {
+	// Mock data
+	mockAttr := slog.Attr{
+		Key:   "mockKey",
+		Value: slog.StringValue("mockValue"),
+	}
+	mockLoggerAttr := []slog.Attr{mockAttr}
+	mockGroups := []string{"group1", "group2"}
+	mockRecord := &slog.Record{
+		Time:    time.Now(),
+		Level:   slog.LevelInfo,
+		Message: "Test message",
+	}
+
+	// Mock replaceAttr function
+	replaceAttr := func(groups []string, a slog.Attr) slog.Attr {
+		return a
+	}
+
+	// Call DefaultConverter function
+	event := DefaultConverter(true, replaceAttr, mockLoggerAttr, mockGroups, mockRecord, nil)
+
+	// Assertions
+	assert.NotNil(t, event)
+	assert.Equal(t, mockRecord.Time.UTC(), event.Timestamp)
+	assert.Equal(t, LogLevels[mockRecord.Level], event.Level)
+	assert.Equal(t, mockRecord.Message, event.Message)
+	assert.Equal(t, name, event.Logger)
+
+	// Check if the attributes are correctly converted
+	var foundMockKey bool
+	for key, value := range event.Extra {
+		if key == "mockKey" && value == "mockValue" {
+			foundMockKey = true
+			break
+		}
+	}
+	assert.True(t, foundMockKey)
+}
+
+func TestAttrToSentryEvent(t *testing.T) {
+	reqURL, _ := url.Parse("http://example.com")
+
+	tests := map[string]struct {
+		attr     slog.Attr
+		expected *sentry.Event
+	}{
+		"dist": {
+			attr:     slog.Attr{Key: "dist", Value: slog.StringValue("dist_value")},
+			expected: &sentry.Event{Dist: "dist_value"},
+		},
+		"environment": {
+			attr:     slog.Attr{Key: "environment", Value: slog.StringValue("env_value")},
+			expected: &sentry.Event{Environment: "env_value"},
+		},
+		"event_id": {
+			attr:     slog.Attr{Key: "event_id", Value: slog.StringValue("event_id_value")},
+			expected: &sentry.Event{EventID: sentry.EventID("event_id_value")},
+		},
+		"platform": {
+			attr:     slog.Attr{Key: "platform", Value: slog.StringValue("platform_value")},
+			expected: &sentry.Event{Platform: "platform_value"},
+		},
+		"release": {
+			attr:     slog.Attr{Key: "release", Value: slog.StringValue("release_value")},
+			expected: &sentry.Event{Release: "release_value"},
+		},
+		"server_name": {
+			attr:     slog.Attr{Key: "server_name", Value: slog.StringValue("server_name_value")},
+			expected: &sentry.Event{ServerName: "server_name_value"},
+		},
+		"tags": {
+			attr: slog.Attr{Key: "tags", Value: slog.GroupValue(
+				slog.Attr{Key: "tag1", Value: slog.StringValue("value1")},
+				slog.Attr{Key: "tag2", Value: slog.StringValue("value2")},
+			)},
+			expected: &sentry.Event{Tags: map[string]string{"tag1": "value1", "tag2": "value2"}},
+		},
+		"transaction": {
+			attr:     slog.Attr{Key: "transaction", Value: slog.StringValue("transaction_value")},
+			expected: &sentry.Event{Transaction: "transaction_value"},
+		},
+		"user": {
+			attr: slog.Attr{Key: "user", Value: slog.GroupValue(
+				slog.Attr{Key: "id", Value: slog.StringValue("user_id")},
+				slog.Attr{Key: "email", Value: slog.StringValue("user_email")},
+				slog.Attr{Key: "ip_address", Value: slog.StringValue("user_ip_address")},
+				slog.Attr{Key: "username", Value: slog.StringValue("user_username")},
+				slog.Attr{Key: "segment", Value: slog.StringValue("user_segment")},
+				slog.Attr{Key: "name", Value: slog.StringValue("user_name")},
+			)},
+			expected: &sentry.Event{
+				User: sentry.User{
+					ID:        "user_id",
+					Email:     "user_email",
+					IPAddress: "user_ip_address",
+					Username:  "user_username",
+					Name:      "user_name",
+					Segment:   "user_segment",
+					Data:      map[string]string{},
+				},
+			},
+		},
+		"request": {
+			attr: slog.Attr{Key: "request", Value: slog.AnyValue(&http.Request{
+				Method: "GET",
+				URL:    reqURL,
+				Header: http.Header{},
+			})},
+			expected: &sentry.Event{Request: &sentry.Request{
+				Method: "GET",
+				URL:    "http://",
+				Headers: map[string]string{
+					"Host": "",
+				},
+			}},
+		},
+		"request_ptr": {
+			attr: slog.Attr{Key: "request", Value: slog.AnyValue(http.Request{
+				Method: "GET",
+				URL:    reqURL,
+				Header: http.Header{},
+			})},
+			expected: &sentry.Event{Request: &sentry.Request{
+				Method: "GET",
+				URL:    "http://",
+				Headers: map[string]string{
+					"Host": "",
+				},
+			}},
+		},
+		"request_str": {
+			attr:     slog.Attr{Key: "request", Value: slog.StringValue("GET http://")},
+			expected: &sentry.Event{Extra: map[string]any{"request": "GET http://"}},
+		},
+		"context_group": {
+			attr: slog.Attr{Key: "context", Value: slog.GroupValue(
+				slog.Attr{Key: "key1", Value: slog.StringValue("value1")},
+				slog.Attr{Key: "key2", Value: slog.StringValue("value2")},
+			)},
+			expected: &sentry.Event{Extra: map[string]any{
+				"context": map[string]any{
+					"key1": "value1",
+					"key2": "value2",
+				}},
+			},
+		},
+	}
+
+	for name, tc := range tests {
+		t.Run(name, func(t *testing.T) {
+			event := sentry.NewEvent()
+			attrToSentryEvent(tc.attr, event)
+			assert.Equal(t, tc.expected.Dist, event.Dist)
+			assert.Equal(t, tc.expected.Environment, event.Environment)
+			assert.Equal(t, tc.expected.EventID, event.EventID)
+			assert.Equal(t, tc.expected.Platform, event.Platform)
+			assert.Equal(t, tc.expected.Release, event.Release)
+			assert.Equal(t, tc.expected.ServerName, event.ServerName)
+			assert.Equal(t, tc.expected.Transaction, event.Transaction)
+			assert.Equal(t, tc.expected.User, event.User)
+			assert.Equal(t, tc.expected.Request, event.Request)
+			if len(tc.expected.Tags) == 0 {
+				assert.Empty(t, event.Tags)
+			} else {
+				assert.Equal(t, tc.expected.Tags, event.Tags)
+			}
+			if len(tc.expected.Extra) == 0 {
+				assert.Empty(t, event.Extra)
+			} else {
+				assert.Equal(t, tc.expected.Extra, event.Extra)
+			}
+		})
+	}
+}
diff --git a/slog/go.mod b/slog/go.mod
new file mode 100644
index 000000000..36f12bdb1
--- /dev/null
+++ slog/go.mod
@@ -0,0 +1,18 @@
+module github.com/getsentry/sentry-go/slog
+
+go 1.21
+
+require (
+	github.com/getsentry/sentry-go v0.30.0
+	github.com/stretchr/testify v1.9.0
+)
+
+replace github.com/getsentry/sentry-go => ../
+
+require (
+	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/pmezard/go-difflib v1.0.0 // indirect
+	golang.org/x/sys v0.18.0 // indirect
+	golang.org/x/text v0,.14.0 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
+)
diff --git a/slog/go.sum b/slog/go.sum
new file mode 100644
index 000000000..c0a42c2de
--- /dev/null
+++ slog/go.sum
@@ -0,0 +1,22 @@
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
+github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
+github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
+golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/slog/sentryslog.go b/slog/sentryslog.go
new file mode 100644
index 000000000..362050f36
--- /dev/null
+++ slog/sentryslog.go
@@ -0,0 +1,136 @@
+package sentryslog
+
+import (
+	"context"
+	"log/slog"
+
+	"github.com/getsentry/sentry-go"
+)
+
+// Majority of the code in this package is derived from https://github.com/samber/slog-sentry AND https://github.com/samber/slog-common
+// MIT License
+
+// Copyright (c) 2023 Samuel Berthe
+
+// 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.
+
+var (
+	_ slog.Handler = (*SentryHandler)(nil)
+
+	LogLevels = map[slog.Level]sentry.Level{
+		slog.LevelDebug: sentry.LevelDebug,
+		slog.LevelInfo:  sentry.LevelInfo,
+		slog.LevelWarn:  sentry.LevelWarning,
+		slog.LevelError: sentry.LevelError,
+	}
+)
+
+type Option struct {
+	// Level sets the minimum log level to capture and send to Sentry.
+	// Logs at this level and above will be processed. The default level is debug.
+	Level slog.Leveler
+
+	// Hub specifies the Sentry Hub to use for capturing events.
+	// If not provided, the current Hub is used by default.
+	Hub *sentry.Hub
+
+	// Converter is an optional function that customizes how log records
+	// are converted into Sentry events. By default, the DefaultConverter is used.
+	Converter Converter
+
+	// AttrFromContext is an optional slice of functions that extract attributes
+	// from the context. These functions can add additional metadata to the log entry.
+	AttrFromContext []func(ctx context.Context) []slog.Attr
+
+	// AddSource is an optional flag that, when set to true, includes the source
+	// information (such as file and line number) in the Sentry event.
+	// This can be useful for debugging purposes.
+	AddSource bool
+
+	// ReplaceAttr is an optional function that allows for the modification or
+	// replacement of attributes in the log record. This can be used to filter
+	// or transform attributes before they are sent to Sentry.
+	ReplaceAttr func(groups []string, a slog.Attr) slog.Attr
+}
+
+func (o Option) NewSentryHandler() slog.Handler {
+	if o.Level == nil {
+		o.Level = slog.LevelDebug
+	}
+
+	if o.Converter == nil {
+		o.Converter = DefaultConverter
+	}
+
+	if o.AttrFromContext == nil {
+		o.AttrFromContext = []func(ctx context.Context) []slog.Attr{}
+	}
+
+	return &SentryHandler{
+		option: o,
+		attrs:  []slog.Attr{},
+		groups: []string{},
+	}
+}
+
+type SentryHandler struct {
+	option Option
+	attrs  []slog.Attr
+	groups []string
+}
+
+func (h *SentryHandler) Enabled(_ context.Context, level slog.Level) bool {
+	return level >= h.option.Level.Level()
+}
+
+func (h *SentryHandler) Handle(ctx context.Context, record slog.Record) error {
+	hub := sentry.CurrentHub()
+	if hubFromContext := sentry.GetHubFromContext(ctx); hubFromContext != nil {
+		hub = hubFromContext
+	} else if h.option.Hub != nil {
+		hub = h.option.Hub
+	}
+
+	fromContext := contextExtractor(ctx, h.option.AttrFromContext)
+	event := h.option.Converter(h.option.AddSource, h.option.ReplaceAttr, append(h.attrs, fromContext...), h.groups, &record, hub)
+	hub.CaptureEvent(event)
+
+	return nil
+}
+
+func (h *SentryHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
+	return &SentryHandler{
+		option: h.option,
+		attrs:  appendAttrsToGroup(h.groups, h.attrs, attrs...),
+		groups: h.groups,
+	}
+}
+
+func (h *SentryHandler) WithGroup(name string) slog.Handler {
+	// https://cs.opensource.google/go/x/exp/+/46b07846:slog/handler.go;l=247
+	if name == "" {
+		return h
+	}
+
+	return &SentryHandler{
+		option: h.option,
+		attrs:  h.attrs,
+		groups: append(h.groups, name),
+	}
+}
diff --git a/slog/sentryslog_test.go b/slog/sentryslog_test.go
new file mode 100644
index 000000000..ecd910a51
--- /dev/null
+++ slog/sentryslog_test.go
@@ -0,0 +1,198 @@
+package sentryslog
+
+import (
+	"context"
+	"fmt"
+	"log/slog"
+	"testing"
+
+	"github.com/getsentry/sentry-go"
+)
+
+func TestSentryHandler_Enabled(t *testing.T) {
+	tests := map[string]struct {
+		handlerLevel slog.Level
+		checkLevel   slog.Level
+		expected     bool
+	}{
+		"LevelDebug, CheckDebug": {
+			handlerLevel: slog.LevelDebug,
+			checkLevel:   slog.LevelDebug,
+			expected:     true,
+		},
+		"LevelInfo, CheckDebug": {
+			handlerLevel: slog.LevelInfo,
+			checkLevel:   slog.LevelDebug,
+			expected:     false,
+		},
+		"LevelError, CheckWarn": {
+			handlerLevel: slog.LevelError,
+			checkLevel:   slog.LevelWarn,
+			expected:     false,
+		},
+		"LevelWarn, CheckError": {
+			handlerLevel: slog.LevelWarn,
+			checkLevel:   slog.LevelError,
+			expected:     true,
+		},
+	}
+
+	for name, tt := range tests {
+		t.Run(name, func(t *testing.T) {
+			h := SentryHandler{option: Option{Level: tt.handlerLevel}}
+			if got := h.Enabled(context.Background(), tt.checkLevel); got != tt.expected {
+				t.Errorf("Enabled() = %v, want %v", got, tt.expected)
+			}
+		})
+	}
+}
+
+func TestSentryHandler_WithAttrs(t *testing.T) {
+	tests := map[string]struct {
+		initialAttrs []slog.Attr
+		newAttrs     []slog.Attr
+		expected     []slog.Attr
+	}{
+		"Empty initial attrs": {
+			initialAttrs: []slog.Attr{},
+			newAttrs:     []slog.Attr{slog.String("key", "value")},
+			expected:     []slog.Attr{slog.String("key", "value")},
+		},
+		"Non-empty initial attrs": {
+			initialAttrs: []slog.Attr{slog.String("existing", "attr")},
+			newAttrs:     []slog.Attr{slog.String("key", "value")},
+			expected:     []slog.Attr{slog.String("existing", "attr"), slog.String("key", "value")},
+		},
+	}
+
+	for name, tt := range tests {
+		t.Run(name, func(t *testing.T) {
+			h := SentryHandler{attrs: tt.initialAttrs}
+			newHandler := h.WithAttrs(tt.newAttrs)
+			if !equalAttrs(newHandler.(*SentryHandler).attrs, tt.expected) {
+				t.Errorf("WithAttrs() = %+v, want %+v", newHandler.(*SentryHandler).attrs, tt.expected)
+			}
+		})
+	}
+}
+
+func TestSentryHandler_WithGroup(t *testing.T) {
+	tests := map[string]struct {
+		initialGroups []string
+		newGroup      string
+		expected      []string
+	}{
+		"Empty initial groups": {
+			initialGroups: []string{},
+			newGroup:      "group1",
+			expected:      []string{"group1"},
+		},
+		"Non-empty initial groups": {
+			initialGroups: []string{"existingGroup"},
+			newGroup:      "newGroup",
+			expected:      []string{"existingGroup", "newGroup"},
+		},
+	}
+
+	for name, tt := range tests {
+		t.Run(name, func(t *testing.T) {
+			h := SentryHandler{groups: tt.initialGroups}
+			newHandler := h.WithGroup(tt.newGroup)
+			if !equalStrings(newHandler.(*SentryHandler).groups, tt.expected) {
+				t.Errorf("WithGroup() = %+v, want %+v", newHandler.(*SentryHandler).groups, tt.expected)
+			}
+		})
+	}
+}
+
+func TestOption_NewSentryHandler(t *testing.T) {
+	tests := map[string]struct {
+		option   Option
+		expected slog.Handler
+	}{
+		"Default options": {
+			option:   Option{},
+			expected: &SentryHandler{option: Option{Level: slog.LevelDebug, Converter: DefaultConverter, AttrFromContext: []func(ctx context.Context) []slog.Attr{}}},
+		},
+		"Custom options": {
+			option: Option{
+				Level:           slog.LevelInfo,
+				Converter:       CustomConverter,
+				AttrFromContext: []func(ctx context.Context) []slog.Attr{customAttrFromContext},
+			},
+			expected: &SentryHandler{
+				option: Option{
+					Level:           slog.LevelInfo,
+					Converter:       CustomConverter,
+					AttrFromContext: []func(ctx context.Context) []slog.Attr{customAttrFromContext},
+				},
+				attrs:  []slog.Attr{},
+				groups: []string{},
+			},
+		},
+	}
+
+	for name, tt := range tests {
+		t.Run(name, func(t *testing.T) {
+			got := tt.option.NewSentryHandler()
+			if !compareHandlers(got, tt.expected) {
+				t.Errorf("NewSentryHandler() = %+v, want %+v", got, tt.expected)
+			}
+		})
+	}
+}
+
+func equalAttrs(a, b []slog.Attr) bool {
+	if len(a) != len(b) {
+		return false
+	}
+	for i := range a {
+		if a[i].Key != b[i].Key || a[i].String() != b[i].String() {
+			return false
+		}
+	}
+	return true
+}
+
+func equalStrings(a, b []string) bool {
+	if len(a) != len(b) {
+		return false
+	}
+	for i := range a {
+		if a[i] != b[i] {
+			return false
+		}
+	}
+	return true
+}
+
+func compareHandlers(h1, h2 slog.Handler) bool {
+	sh1, ok1 := h1.(*SentryHandler)
+	sh2, ok2 := h2.(*SentryHandler)
+	if !ok1 || !ok2 {
+		return false
+	}
+	return sh1.option.Level == sh2.option.Level &&
+		equalFuncs(sh1.option.AttrFromContext, sh2.option.AttrFromContext)
+}
+
+func equalFuncs(a, b []func(ctx context.Context) []slog.Attr) bool {
+	if len(a) != len(b) {
+		return false
+	}
+	for i := range a {
+		if fmt.Sprintf("%p", a[i]) != fmt.Sprintf("%p", b[i]) {
+			return false
+		}
+	}
+	return true
+}
+
+// Mock functions for custom converter and custom attr from context.
+func CustomConverter(bool, func([]string, slog.Attr) slog.Attr, []slog.Attr, []string, *slog.Record, *sentry.Hub) *sentry.Event {
+	return sentry.NewEvent()
+}
+
+func customAttrFromContext(context.Context) []slog.Attr {
+	return []slog.Attr{slog.String("custom", "attr")}
+}
diff --git stacktrace.go stacktrace.go
index 3b372bcff..f59e23664 100644
--- stacktrace.go
+++ stacktrace.go
@@ -4,6 +4,7 @@ import (
 	"go/build"
 	"reflect"
 	"runtime"
+	"slices"
 	"strings"
 )
 
@@ -277,12 +278,7 @@ func extractFrames(pcs []uintptr) []runtime.Frame {
 		}
 	}
 
-	// TODO don't append and reverse, put in the right place from the start.
-	// reverse
-	for i, j := 0, len(frames)-1; i < j; i, j = i+1, j-1 {
-		frames[i], frames[j] = frames[j], frames[i]
-	}
-
+	slices.Reverse(frames)
 	return frames
 }
 
@@ -336,12 +332,10 @@ func shouldSkipFrame(module string) bool {
 var goRoot = strings.ReplaceAll(build.Default.GOROOT, "\\", "/")
 
 func setInAppFrame(frame *Frame) {
-	if strings.HasPrefix(frame.AbsPath, goRoot) ||
-		strings.Contains(frame.Module, "vendor") ||
+	frame.InApp = true
+	if strings.HasPrefix(frame.AbsPath, goRoot) || strings.Contains(frame.Module, "vendor") ||
 		strings.Contains(frame.Module, "third_party") {
 		frame.InApp = false
-	} else {
-		frame.InApp = true
 	}
 }
 
@@ -382,3 +376,32 @@ func baseName(name string) string {
 	}
 	return name
 }
+
+func isCompilerGeneratedSymbol(name string) bool {
+	// In versions of Go 1.20 and above a prefix of "type:" and "go:" is a
+	// compiler-generated symbol that doesn't belong to any package.
+	// See variable reservedimports in cmd/compile/internal/gc/subr.go
+	if strings.HasPrefix(name, "go:") || strings.HasPrefix(name, "type:") {
+		return true
+	}
+	return false
+}
+
+// Walk backwards through the results and for the current function name
+// remove it's parent function's prefix, leaving only it's actual name. This
+// fixes issues grouping errors with the new fully qualified function names
+// introduced from Go 1.21.
+func cleanupFunctionNamePrefix(f []Frame) []Frame {
+	for i := len(f) - 1; i > 0; i-- {
+		name := f[i].Function
+		parentName := f[i-1].Function + "."
+
+		if !strings.HasPrefix(name, parentName) {
+			continue
+		}
+
+		f[i].Function = name[len(parentName):]
+	}
+
+	return f
+}
diff --git stacktrace_below_go1.20.go stacktrace_below_go1.20.go
deleted file mode 100644
index f6fb8e1e4..000000000
--- stacktrace_below_go1.20.go
+++ /dev/null
@@ -1,15 +0,0 @@
-//go:build !go1.20
-
-package sentry
-
-import "strings"
-
-func isCompilerGeneratedSymbol(name string) bool {
-	// In versions of Go below 1.20 a prefix of "type." and "go." is a
-	// compiler-generated symbol that doesn't belong to any package.
-	// See variable reservedimports in cmd/compile/internal/gc/subr.go
-	if strings.HasPrefix(name, "go.") || strings.HasPrefix(name, "type.") {
-		return true
-	}
-	return false
-}
diff --git stacktrace_below_go1.20_test.go stacktrace_below_go1.20_test.go
deleted file mode 100644
index 2f350e3c8..000000000
--- stacktrace_below_go1.20_test.go
+++ /dev/null
@@ -1,32 +0,0 @@
-//go:build !go1.20
-
-package sentry
-
-import (
-	"testing"
-
-	"github.com/google/go-cmp/cmp"
-)
-
-func TestFilterCompilerGeneratedSymbols(t *testing.T) {
-	tests := []struct {
-		symbol              string
-		expectedPackageName string
-	}{
-		{"type..eq.[9]debug/elf.intName", ""},
-		{"type..hash.debug/elf.ProgHeader", ""},
-		{"type..eq.runtime._panic", ""},
-		{"type..hash.struct { runtime.gList; runtime.n int32 }", ""},
-		{"go.(*struct { sync.Mutex; math/big.table [64]math/big", ""},
-		{"github.com/getsentry/sentry-go.Test.func2.1.1", "github.com/getsentry/sentry-go"},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.symbol, func(t *testing.T) {
-			packageName := packageName(tt.symbol)
-			if diff := cmp.Diff(tt.expectedPackageName, packageName); diff != "" {
-				t.Errorf("Package name mismatch (-want +got):\n%s", diff)
-			}
-		})
-	}
-}
diff --git stacktrace_below_go1.21.go stacktrace_below_go1.21.go
deleted file mode 100644
index 35a42e4dd..000000000
--- stacktrace_below_go1.21.go
+++ /dev/null
@@ -1,7 +0,0 @@
-//go:build !go1.21
-
-package sentry
-
-func cleanupFunctionNamePrefix(f []Frame) []Frame {
-	return f
-}
diff --git stacktrace_below_go1.21_test.go stacktrace_below_go1.21_test.go
deleted file mode 100644
index 35d50d311..000000000
--- stacktrace_below_go1.21_test.go
+++ /dev/null
@@ -1,17 +0,0 @@
-//go:build !go1.21
-
-package sentry
-
-import (
-	"testing"
-)
-
-func Test_cleanupFunctionNamePrefix(t *testing.T) {
-	f := []Frame{
-		{Function: "main.main"},
-		{Function: "main.main.func1"},
-	}
-	got := cleanupFunctionNamePrefix(f)
-	assertEqual(t, got, f)
-
-}
diff --git stacktrace_go1.20.go stacktrace_go1.20.go
deleted file mode 100644
index ff1cbf600..000000000
--- stacktrace_go1.20.go
+++ /dev/null
@@ -1,15 +0,0 @@
-//go:build go1.20
-
-package sentry
-
-import "strings"
-
-func isCompilerGeneratedSymbol(name string) bool {
-	// In versions of Go 1.20 and above a prefix of "type:" and "go:" is a
-	// compiler-generated symbol that doesn't belong to any package.
-	// See variable reservedimports in cmd/compile/internal/gc/subr.go
-	if strings.HasPrefix(name, "go:") || strings.HasPrefix(name, "type:") {
-		return true
-	}
-	return false
-}
diff --git stacktrace_go1.20_test.go stacktrace_go1.20_test.go
deleted file mode 100644
index 245dbe148..000000000
--- stacktrace_go1.20_test.go
+++ /dev/null
@@ -1,32 +0,0 @@
-//go:build go1.20
-
-package sentry
-
-import (
-	"testing"
-
-	"github.com/google/go-cmp/cmp"
-)
-
-func TestFilterCompilerGeneratedSymbols(t *testing.T) {
-	tests := []struct {
-		symbol              string
-		expectedPackageName string
-	}{
-		{"type:.eq.[9]debug/elf.intName", ""},
-		{"type:.hash.debug/elf.ProgHeader", ""},
-		{"type:.eq.runtime._panic", ""},
-		{"type:.hash.struct { runtime.gList; runtime.n int32 }", ""},
-		{"go:(*struct { sync.Mutex; math/big.table [64]math/big", ""},
-		{"go.uber.org/zap/buffer.(*Buffer).AppendString", "go.uber.org/zap/buffer"},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.symbol, func(t *testing.T) {
-			packageName := packageName(tt.symbol)
-			if diff := cmp.Diff(tt.expectedPackageName, packageName); diff != "" {
-				t.Errorf("Package name mismatch (-want +got):\n%s", diff)
-			}
-		})
-	}
-}
diff --git stacktrace_go1.21.go stacktrace_go1.21.go
deleted file mode 100644
index 45147c850..000000000
--- stacktrace_go1.21.go
+++ /dev/null
@@ -1,25 +0,0 @@
-//go:build go1.21
-
-package sentry
-
-import "strings"
-
-// 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
-}
diff --git stacktrace_go1.21_test.go stacktrace_go1.21_test.go
deleted file mode 100644
index 67c159900..000000000
--- stacktrace_go1.21_test.go
+++ /dev/null
@@ -1,81 +0,0 @@
-//go:build go1.21
-
-package sentry
-
-import (
-	"testing"
-)
-
-func Test_cleanupFunctionNamePrefix(t *testing.T) {
-	cases := map[string]struct {
-		f    []Frame
-		want []Frame
-	}{
-		"SimpleCase": {
-			f: []Frame{
-				{Function: "main.main"},
-				{Function: "main.main.func1"},
-			},
-			want: []Frame{
-				{Function: "main.main"},
-				{Function: "func1"},
-			},
-		},
-		"MultipleLevels": {
-			f: []Frame{
-				{Function: "main.main"},
-				{Function: "main.main.func1"},
-				{Function: "main.main.func1.func2"},
-			},
-			want: []Frame{
-				{Function: "main.main"},
-				{Function: "func1"},
-				{Function: "func2"},
-			},
-		},
-		"PrefixWithRun": {
-			f: []Frame{
-				{Function: "Run.main"},
-				{Function: "Run.main.func1"},
-			},
-			want: []Frame{
-				{Function: "Run.main"},
-				{Function: "func1"},
-			},
-		},
-		"NoPrefixMatch": {
-			f: []Frame{
-				{Function: "main.main"},
-				{Function: "main.handler"},
-			},
-			want: []Frame{
-				{Function: "main.main"},
-				{Function: "main.handler"},
-			},
-		},
-		"SingleFrame": {
-			f: []Frame{
-				{Function: "main.main"},
-			},
-			want: []Frame{
-				{Function: "main.main"},
-			},
-		},
-		"ComplexPrefix": {
-			f: []Frame{
-				{Function: "app.package.Run"},
-				{Function: "app.package.Run.Logger.func1"},
-			},
-			want: []Frame{
-				{Function: "app.package.Run"},
-				{Function: "Logger.func1"},
-			},
-		},
-	}
-	for name, tt := range cases {
-		t.Run(name, func(t *testing.T) {
-			got := cleanupFunctionNamePrefix(tt.f)
-			assertEqual(t, got, tt.want)
-		})
-	}
-}
diff --git stacktrace_test.go stacktrace_test.go
index 96d682801..c9e31bf9d 100644
--- stacktrace_test.go
+++ stacktrace_test.go
@@ -228,3 +228,100 @@ func TestEventWithExceptionStacktraceMarshalJSON(t *testing.T) {
 		t.Errorf("Event mismatch (-want +got):\n%s", diff)
 	}
 }
+
+func TestFilterCompilerGeneratedSymbols(t *testing.T) {
+	tests := []struct {
+		symbol              string
+		expectedPackageName string
+	}{
+		{"type:.eq.[9]debug/elf.intName", ""},
+		{"type:.hash.debug/elf.ProgHeader", ""},
+		{"type:.eq.runtime._panic", ""},
+		{"type:.hash.struct { runtime.gList; runtime.n int32 }", ""},
+		{"go:(*struct { sync.Mutex; math/big.table [64]math/big", ""},
+		{"go.uber.org/zap/buffer.(*Buffer).AppendString", "go.uber.org/zap/buffer"},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.symbol, func(t *testing.T) {
+			packageName := packageName(tt.symbol)
+			if diff := cmp.Diff(tt.expectedPackageName, packageName); diff != "" {
+				t.Errorf("Package name mismatch (-want +got):\n%s", diff)
+			}
+		})
+	}
+}
+
+func Test_cleanupFunctionNamePrefix(t *testing.T) {
+	cases := map[string]struct {
+		f    []Frame
+		want []Frame
+	}{
+		"SimpleCase": {
+			f: []Frame{
+				{Function: "main.main"},
+				{Function: "main.main.func1"},
+			},
+			want: []Frame{
+				{Function: "main.main"},
+				{Function: "func1"},
+			},
+		},
+		"MultipleLevels": {
+			f: []Frame{
+				{Function: "main.main"},
+				{Function: "main.main.func1"},
+				{Function: "main.main.func1.func2"},
+			},
+			want: []Frame{
+				{Function: "main.main"},
+				{Function: "func1"},
+				{Function: "func2"},
+			},
+		},
+		"PrefixWithRun": {
+			f: []Frame{
+				{Function: "Run.main"},
+				{Function: "Run.main.func1"},
+			},
+			want: []Frame{
+				{Function: "Run.main"},
+				{Function: "func1"},
+			},
+		},
+		"NoPrefixMatch": {
+			f: []Frame{
+				{Function: "main.main"},
+				{Function: "main.handler"},
+			},
+			want: []Frame{
+				{Function: "main.main"},
+				{Function: "main.handler"},
+			},
+		},
+		"SingleFrame": {
+			f: []Frame{
+				{Function: "main.main"},
+			},
+			want: []Frame{
+				{Function: "main.main"},
+			},
+		},
+		"ComplexPrefix": {
+			f: []Frame{
+				{Function: "app.package.Run"},
+				{Function: "app.package.Run.Logger.func1"},
+			},
+			want: []Frame{
+				{Function: "app.package.Run"},
+				{Function: "Logger.func1"},
+			},
+		},
+	}
+	for name, tt := range cases {
+		t.Run(name, func(t *testing.T) {
+			got := cleanupFunctionNamePrefix(tt.f)
+			assertEqual(t, got, tt.want)
+		})
+	}
+}
diff --git tracing.go tracing.go
index b30ceb79f..79a1bf3a0 100644
--- tracing.go
+++ tracing.go
@@ -735,31 +735,32 @@ const (
 	maxSpanStatus
 )
 
+var spanStatuses = [maxSpanStatus]string{
+	"",
+	"ok",
+	"cancelled", // [sic]
+	"unknown",
+	"invalid_argument",
+	"deadline_exceeded",
+	"not_found",
+	"already_exists",
+	"permission_denied",
+	"resource_exhausted",
+	"failed_precondition",
+	"aborted",
+	"out_of_range",
+	"unimplemented",
+	"internal_error",
+	"unavailable",
+	"data_loss",
+	"unauthenticated",
+}
+
 func (ss SpanStatus) String() string {
 	if ss >= maxSpanStatus {
 		return ""
 	}
-	m := [maxSpanStatus]string{
-		"",
-		"ok",
-		"cancelled", // [sic]
-		"unknown",
-		"invalid_argument",
-		"deadline_exceeded",
-		"not_found",
-		"already_exists",
-		"permission_denied",
-		"resource_exhausted",
-		"failed_precondition",
-		"aborted",
-		"out_of_range",
-		"unimplemented",
-		"internal_error",
-		"unavailable",
-		"data_loss",
-		"unauthenticated",
-	}
-	return m[ss]
+	return spanStatuses[ss]
 }
 
 func (ss SpanStatus) MarshalJSON() ([]byte, error) {
diff --git transport.go transport.go
index 02fc1d4f1..25fe2d321 100644
--- transport.go
+++ transport.go
@@ -141,7 +141,7 @@ func encodeMetric(enc *json.Encoder, b io.Writer, metrics []Metric) error {
 		return err
 	}
 
-	return err
+	return nil
 }
 
 func encodeAttachment(enc *json.Encoder, b io.Writer, attachment *Attachment) error {
@@ -279,10 +279,12 @@ func getRequestFromEvent(ctx context.Context, event *Event, dsn *Dsn) (r *http.R
 			r.Header.Set("X-Sentry-Auth", auth)
 		}
 	}()
+
 	body := getRequestBodyFromEvent(event)
 	if body == nil {
 		return nil, errors.New("event could not be marshaled")
 	}
+
 	envelope, err := envelopeFromBody(event, dsn, time.Now(), body)
 	if err != nil {
 		return nil, err
diff --git a/zerolog/README.md b/zerolog/README.md
new file mode 100644
index 000000000..18e4b0d08
--- /dev/null
+++ zerolog/README.md
@@ -0,0 +1,88 @@
+<p align="center">
+  <a href="https://sentry.io" target="_blank" align="center">
+    <img src="https://sentry-brand.storage.googleapis.com/sentry-logo-black.png" width="280">
+  </a>
+  <br />
+</p>
+
+# Official Sentry Zerolog Writer for Sentry-Go SDK
+
+**Go.dev Documentation:** https://pkg.go.dev/github.com/getsentry/sentryzerolog  
+**Example Usage:** https://github.com/getsentry/sentry-go/tree/master/_examples/zerolog
+
+## Overview
+
+This package provides a writer for the [Zerolog](https://github.com/rs/zerolog) logger, enabling seamless integration with [Sentry](https://sentry.io). With this writer, logs at specific levels can be captured as Sentry events, while others can be added as breadcrumbs for enhanced context.
+
+## Installation
+
+```sh
+go get github.com/getsentry/sentry-go/zerolog
+```
+
+## Usage
+
+```go
+package main
+
+import (
+	"time"
+
+	"github.com/rs/zerolog"
+	"github.com/getsentry/sentry-go"
+	sentryzerolog "github.com/getsentry/sentry-go/zerolog"
+)
+
+func main() {
+	// Initialize Sentry
+	err := sentry.Init(sentry.ClientOptions{
+		Dsn: "your-public-dsn",
+	})
+	if err != nil {
+		panic(err)
+	}
+	defer sentry.Flush(2 * time.Second)
+
+	// Configure Sentry Zerolog Writer
+	writer, err := sentryzerolog.New(sentryzerolog.Config{
+		ClientOptions: sentry.ClientOptions{
+			Dsn:   "your-public-dsn",
+			Debug: true,
+		},
+		Options: sentryzerolog.Options{
+			Levels:         []zerolog.Level{zerolog.ErrorLevel, zerolog.FatalLevel},
+			FlushTimeout:   3 * time.Second,
+			WithBreadcrumbs: true,
+		},
+	})
+	if err != nil {
+		panic(err)
+	}
+	defer writer.Close()
+
+	// Initialize Zerolog
+	logger := zerolog.New(writer).With().Timestamp().Logger()
+
+	// Example Logs
+	logger.Info().Msg("This is an info message")           // Breadcrumb
+	logger.Error().Msg("This is an error message")         // Captured as an event
+	logger.Fatal().Msg("This is a fatal message")          // Captured as an event and flushes
+}
+```
+
+## Configuration
+
+The `sentryzerolog.New` function accepts a `sentryzerolog.Config` struct, which allows for the following configuration options:
+
+- `ClientOptions`: A struct of `sentry.ClientOptions` that allows you to configure how the Sentry client will behave.
+- `Options`: A struct of `sentryzerolog.Options` that allows you to configure how the Sentry Zerolog writer will behave.
+
+The `sentryzerolog.Options` struct allows you to configure the following:
+
+- `Levels`: An array of `zerolog.Level` that defines which log levels should be sent to Sentry.
+- `FlushTimeout`: A `time.Duration` that defines how long to wait before flushing events.
+- `WithBreadcrumbs`: A `bool` that enables or disables adding logs as breadcrumbs for contextual logging. Non-event logs will appear as breadcrumbs in Sentry.
+
+## Notes
+
+- Always call Flush to ensure all events are sent to Sentry before program termination
\ No newline at end of file
diff --git a/zerolog/go.mod b/zerolog/go.mod
new file mode 100644
index 000000000..794ae7674
--- /dev/null
+++ zerolog/go.mod
@@ -0,0 +1,22 @@
+module github.com/getsentry/sentry-go/zerolog
+
+go 1.21
+
+require (
+	github.com/buger/jsonparser v1.1.1
+	github.com/getsentry/sentry-go v0.30.0
+	github.com/rs/zerolog v1.33.0
+	github.com/stretchr/testify v1.9.0
+)
+
+replace github.com/getsentry/sentry-go => ../
+
+require (
+	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/mattn/go-colorable v0.1.13 // indirect
+	github.com/mattn/go-isatty v0.0.20 // indirect
+	github.com/pmezard/go-difflib v1.0.0 // indirect
+	golang.org/x/sys v0.18.0 // indirect
+	golang.org/x/text v0.14.0 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
+)
diff --git a/zerolog/go.sum b/zerolog/go.sum
new file mode 100644
index 000000000..5afe84411
--- /dev/null
+++ zerolog/go.sum
@@ -0,0 +1,38 @@
+github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
+github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
+github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
+github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
+github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
+github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
+github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
+github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
+github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
+golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/zerolog/sentryzerolog.go b/zerolog/sentryzerolog.go
new file mode 100644
index 000000000..11bd2453c
--- /dev/null
+++ zerolog/sentryzerolog.go
@@ -0,0 +1,299 @@
+package sentryzerolog
+
+import (
+	"encoding/json"
+	"errors"
+	"io"
+	"time"
+
+	"github.com/buger/jsonparser"
+	sentry "github.com/getsentry/sentry-go"
+	"github.com/rs/zerolog"
+)
+
+// A large portion of this implementation has been taken from https://github.com/archdx/zerolog-sentry/blob/master/writer.go
+
+var (
+	// ErrFlushTimeout is returned when the flush operation times out.
+	ErrFlushTimeout = errors.New("sentryzerolog flush timeout")
+
+	// levels maps zerolog levels to sentry levels.
+	levelsMapping = map[zerolog.Level]sentry.Level{
+		zerolog.TraceLevel: sentry.LevelDebug,
+		zerolog.DebugLevel: sentry.LevelDebug,
+		zerolog.InfoLevel:  sentry.LevelInfo,
+		zerolog.WarnLevel:  sentry.LevelWarning,
+		zerolog.ErrorLevel: sentry.LevelError,
+		zerolog.FatalLevel: sentry.LevelFatal,
+		zerolog.PanicLevel: sentry.LevelFatal,
+	}
+
+	// Ensure that the Writer implements the io.WriteCloser interface.
+	_ = io.WriteCloser(new(Writer))
+
+	now = time.Now
+)
+
+// The identifier of the Zerolog SDK.
+const sdkIdentifier = "sentry.go.zerolog"
+
+// These default log field keys are used to pass specific metadata in a way that
+// Sentry understands. If they are found in the log fields, and the value is of
+// the expected datatype, it will be converted from a generic field, into Sentry
+// metadata.
+const (
+	// FieldRequest holds an *http.Request.
+	FieldRequest = "request"
+	// FieldUser holds a User or *User value.
+	FieldUser = "user"
+	// FieldTransaction holds a transaction ID as a string.
+	FieldTransaction = "transaction"
+	// FieldFingerprint holds a string slice ([]string), used to dictate the
+	// grouping of this event.
+	FieldFingerprint = "fingerprint"
+
+	// These fields are simply omitted, as they are duplicated by the Sentry SDK.
+	FieldGoVersion = "go_version"
+	FieldMaxProcs  = "go_maxprocs"
+
+	// Name of the logger used by the Sentry SDK.
+	logger = "zerolog"
+)
+
+type Config struct {
+	sentry.ClientOptions
+	Options
+}
+
+type Options struct {
+	// Levels specifies the log levels that will trigger event sending to Sentry.
+	// Only log messages at these levels will be sent. By default, the levels are
+	// Error, Fatal, and Panic.
+	Levels []zerolog.Level
+
+	// WithBreadcrumbs, when enabled, adds log entries as breadcrumbs in Sentry.
+	// Breadcrumbs provide a trail of events leading up to an error, which can
+	// be invaluable for understanding the context of issues.
+	WithBreadcrumbs bool
+
+	// FlushTimeout sets the maximum duration allowed for flushing events to Sentry.
+	// This is the time limit within which all pending events must be sent to Sentry
+	// before the application exits. A typical use is ensuring all logs are sent before
+	// application shutdown. The default timeout is usually 3 seconds.
+	FlushTimeout time.Duration
+}
+
+func (o *Options) SetDefaults() {
+	if len(o.Levels) == 0 {
+		o.Levels = []zerolog.Level{
+			zerolog.ErrorLevel,
+			zerolog.FatalLevel,
+			zerolog.PanicLevel,
+		}
+	}
+
+	if o.FlushTimeout == 0 {
+		o.FlushTimeout = 3 * time.Second
+	}
+}
+
+// New creates writer with provided DSN and options.
+func New(cfg Config) (*Writer, error) {
+	client, err := sentry.NewClient(cfg.ClientOptions)
+	if err != nil {
+		return nil, err
+	}
+
+	client.SetSDKIdentifier(sdkIdentifier)
+
+	cfg.Options.SetDefaults()
+
+	levels := make(map[zerolog.Level]struct{}, len(cfg.Levels))
+	for _, lvl := range cfg.Levels {
+		levels[lvl] = struct{}{}
+	}
+
+	return &Writer{
+		hub:             sentry.NewHub(client, sentry.NewScope()),
+		levels:          levels,
+		flushTimeout:    cfg.FlushTimeout,
+		withBreadcrumbs: cfg.WithBreadcrumbs,
+	}, nil
+}
+
+// NewWithHub creates a writer using an existing sentry Hub and options.
+func NewWithHub(hub *sentry.Hub, opts Options) (*Writer, error) {
+	if hub == nil {
+		return nil, errors.New("hub cannot be nil")
+	}
+
+	opts.SetDefaults()
+
+	levels := make(map[zerolog.Level]struct{}, len(opts.Levels))
+	for _, lvl := range opts.Levels {
+		levels[lvl] = struct{}{}
+	}
+
+	return &Writer{
+		hub:             hub,
+		levels:          levels,
+		flushTimeout:    opts.FlushTimeout,
+		withBreadcrumbs: opts.WithBreadcrumbs,
+	}, nil
+}
+
+// Writer is a sentry events writer with std io.Writer interface.
+type Writer struct {
+	hub             *sentry.Hub
+	levels          map[zerolog.Level]struct{}
+	flushTimeout    time.Duration
+	withBreadcrumbs bool
+}
+
+// addBreadcrumb adds event as a breadcrumb.
+func (w *Writer) addBreadcrumb(event *sentry.Event) {
+	if !w.withBreadcrumbs {
+		return
+	}
+
+	breadcrumbType := "default"
+	switch event.Level {
+	case sentry.LevelFatal, sentry.LevelError:
+		breadcrumbType = "error"
+	}
+
+	category, _ := event.Extra["category"].(string)
+
+	w.hub.AddBreadcrumb(&sentry.Breadcrumb{
+		Type:     breadcrumbType,
+		Category: category,
+		Message:  event.Message,
+		Level:    event.Level,
+		Data:     event.Extra,
+	}, nil)
+}
+
+// Write handles zerolog's json and sends events to sentry.
+func (w *Writer) Write(data []byte) (int, error) {
+	n := len(data)
+
+	lvl, err := parseLogLevel(data)
+	if err != nil {
+		return n, nil
+	}
+
+	event, ok := parseLogEvent(data)
+	if !ok {
+		return n, nil
+	}
+
+	event.Level, ok = levelsMapping[lvl]
+	if !ok {
+		return n, nil
+	}
+
+	if _, enabled := w.levels[lvl]; !enabled {
+		// if the level is not enabled, add event as a breadcrumb
+		w.addBreadcrumb(event)
+		return n, nil
+	}
+
+	w.hub.CaptureEvent(event)
+	// should flush before os.Exit
+	if event.Level == sentry.LevelFatal {
+		w.hub.Flush(w.flushTimeout)
+	}
+
+	return n, nil
+}
+
+func (w *Writer) WriteLevel(level zerolog.Level, p []byte) (int, error) {
+	n := len(p)
+
+	event, ok := parseLogEvent(p)
+	if !ok {
+		return n, nil
+	}
+
+	event.Level, ok = levelsMapping[level]
+	if !ok {
+		return n, nil
+	}
+
+	if _, enabled := w.levels[level]; !enabled {
+		// if the level is not enabled, add event as a breadcrumb
+		w.addBreadcrumb(event)
+		return n, nil
+	}
+
+	w.hub.CaptureEvent(event)
+	// should flush before os.Exit
+	if event.Level == sentry.LevelFatal {
+		w.hub.Flush(w.flushTimeout)
+	}
+
+	return n, nil
+}
+
+// Close forces client to flush all pending events.
+// Can be useful before application exits.
+func (w *Writer) Close() error {
+	if ok := w.hub.Flush(w.flushTimeout); !ok {
+		return ErrFlushTimeout
+	}
+	return nil
+}
+
+func parseLogLevel(data []byte) (zerolog.Level, error) {
+	level, err := jsonparser.GetUnsafeString(data, zerolog.LevelFieldName)
+	if err != nil {
+		return zerolog.Disabled, nil
+	}
+
+	return zerolog.ParseLevel(level)
+}
+
+func parseLogEvent(data []byte) (*sentry.Event, bool) {
+	event := sentry.Event{
+		Timestamp: now(),
+		Logger:    logger,
+		Extra:     map[string]any{},
+	}
+
+	err := jsonparser.ObjectEach(data, func(key, value []byte, _ jsonparser.ValueType, _ int) error {
+		k := string(key)
+		switch k {
+		case zerolog.MessageFieldName:
+			event.Message = string(value)
+		case zerolog.ErrorFieldName:
+			event.Exception = append(event.Exception, sentry.Exception{
+				Value:      string(value),
+				Stacktrace: sentry.NewStacktrace(),
+			})
+		case zerolog.LevelFieldName, zerolog.TimestampFieldName:
+		case FieldUser:
+			var user sentry.User
+			err := json.Unmarshal(value, &user)
+			if err != nil {
+				event.Extra[k] = string(value)
+			} else {
+				event.User = user
+			}
+		case FieldTransaction:
+			event.Transaction = string(value)
+		case FieldFingerprint:
+			var fp []string
+			err := json.Unmarshal(value, &fp)
+			if err != nil {
+				event.Extra[k] = string(value)
+			} else {
+				event.Fingerprint = fp
+			}
+		case FieldGoVersion, FieldMaxProcs:
+		default:
+			event.Extra[k] = string(value)
+		}
+		return nil
+	})
+	return &event, err == nil
+}
diff --git a/zerolog/sentryzerolog_test.go b/zerolog/sentryzerolog_test.go
new file mode 100644
index 000000000..b347f5727
--- /dev/null
+++ zerolog/sentryzerolog_test.go
@@ -0,0 +1,342 @@
+package sentryzerolog
+
+import (
+	"errors"
+	"io"
+	"testing"
+	"time"
+
+	"github.com/getsentry/sentry-go"
+	"github.com/rs/zerolog"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+// A large portion of this implementation has been taken from https://github.com/archdx/zerolog-sentry/blob/master/writer_test.go
+
+var logEventJSON = []byte(`{"level":"error","requestId":"bee07485-2485-4f64-99e1-d10165884ca7","error":"dial timeout","time":"2020-06-25T17:19:00+03:00","message":"test message"}`)
+
+func TestParseLogEvent(t *testing.T) {
+	ts := time.Now()
+
+	now = func() time.Time { return ts }
+
+	_, err := New(Config{})
+	require.Nil(t, err)
+
+	ev, ok := parseLogEvent(logEventJSON)
+	require.True(t, ok)
+	zLevel, err := parseLogLevel(logEventJSON)
+	assert.Nil(t, err)
+	ev.Level = levelsMapping[zLevel]
+
+	assert.Equal(t, ts, ev.Timestamp)
+	assert.Equal(t, sentry.LevelError, ev.Level)
+	assert.Equal(t, "zerolog", ev.Logger)
+	assert.Equal(t, "test message", ev.Message)
+
+	require.Len(t, ev.Exception, 1)
+	assert.Equal(t, "dial timeout", ev.Exception[0].Value)
+
+	require.Len(t, ev.Extra, 1)
+	assert.Equal(t, "bee07485-2485-4f64-99e1-d10165884ca7", ev.Extra["requestId"])
+}
+
+func TestFailedClientCreation(t *testing.T) {
+	_, err := New(Config{ClientOptions: sentry.ClientOptions{Dsn: "invalid"}})
+	require.NotNil(t, err)
+}
+
+func TestNewWithHub(t *testing.T) {
+	hub := sentry.CurrentHub()
+	require.NotNil(t, hub)
+
+	_, err := NewWithHub(hub, Options{
+		Levels: []zerolog.Level{zerolog.ErrorLevel},
+	})
+	require.Nil(t, err)
+
+	_, err = NewWithHub(nil, Options{})
+	require.NotNil(t, err)
+}
+
+func TestParseLogLevel(t *testing.T) {
+	_, err := New(Config{})
+	require.Nil(t, err)
+
+	level, err := parseLogLevel(logEventJSON)
+	require.Nil(t, err)
+	assert.Equal(t, zerolog.ErrorLevel, level)
+}
+
+func TestWrite(t *testing.T) {
+	var beforeSendCalled bool
+	cfg := Config{
+		ClientOptions: sentry.ClientOptions{
+			BeforeSend: func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event {
+				assert.Equal(t, sentry.LevelError, event.Level)
+				assert.Equal(t, "test message", event.Message)
+				require.Len(t, event.Exception, 1)
+				assert.Equal(t, "dial timeout", event.Exception[0].Value)
+				assert.True(t, time.Since(event.Timestamp).Minutes() < 1)
+				assert.Equal(t, "bee07485-2485-4f64-99e1-d10165884ca7", event.Extra["requestId"])
+				beforeSendCalled = true
+				return event
+			},
+		},
+		Options: Options{
+			WithBreadcrumbs: true,
+		},
+	}
+	writer, err := New(cfg)
+	require.Nil(t, err)
+
+	var zerologError error
+	zerolog.ErrorHandler = func(err error) {
+		zerologError = err
+	}
+
+	// use io.MultiWriter to enforce using the Write() method
+	log := zerolog.New(io.MultiWriter(writer)).With().Timestamp().
+		Str("requestId", "bee07485-2485-4f64-99e1-d10165884ca7").
+		Interface("user", sentry.User{ID: "1", Email: "testuser@sentry.io"}).
+		Strs("fingerprint", []string{"test"}).
+		Logger()
+	log.Err(errors.New("dial timeout")).
+		Msg("test message")
+
+	require.Nil(t, zerologError)
+	require.True(t, beforeSendCalled)
+}
+
+func TestClose(t *testing.T) {
+	cfg := Config{}
+	writer, err := New(cfg)
+	require.Nil(t, err)
+
+	err = writer.Close()
+	require.Nil(t, err)
+}
+
+func TestWrite_TraceDoesNotPanic(t *testing.T) {
+	var beforeSendCalled bool
+	cfg := Config{
+		ClientOptions: sentry.ClientOptions{
+			BeforeSend: func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event {
+				beforeSendCalled = true
+				return event
+			},
+		},
+		Options: Options{
+			WithBreadcrumbs: false,
+		},
+	}
+	writer, err := New(cfg)
+	require.Nil(t, err)
+
+	var zerologError error
+	zerolog.ErrorHandler = func(err error) {
+		zerologError = err
+	}
+
+	// use io.MultiWriter to enforce using the Write() method
+	log := zerolog.New(io.MultiWriter(writer)).With().Timestamp().
+		Str("requestId", "bee07485-2485-4f64-99e1-d10165884ca7").
+		Str("user", "1").
+		Str("transaction", "test").
+		Str("fingerprint", "test").
+		Logger()
+	log.Trace().Msg("test message")
+
+	require.Nil(t, zerologError)
+	require.False(t, beforeSendCalled)
+}
+
+func TestWriteLevel(t *testing.T) {
+	var beforeSendCalled bool
+	cfg := Config{
+		ClientOptions: sentry.ClientOptions{
+			BeforeSend: func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event {
+				assert.Equal(t, sentry.LevelError, event.Level)
+				assert.Equal(t, "test message", event.Message)
+				require.Len(t, event.Exception, 1)
+				assert.Equal(t, "dial timeout", event.Exception[0].Value)
+				assert.True(t, time.Since(event.Timestamp).Minutes() < 1)
+				assert.Equal(t, "bee07485-2485-4f64-99e1-d10165884ca7", event.Extra["requestId"])
+				beforeSendCalled = true
+				return event
+			},
+		},
+		Options: Options{
+			WithBreadcrumbs: true,
+		},
+	}
+	writer, err := New(cfg)
+	require.Nil(t, err)
+
+	var zerologError error
+	zerolog.ErrorHandler = func(err error) { zerologError = err }
+
+	log := zerolog.New(writer).With().Timestamp().
+		Str("requestId", "bee07485-2485-4f64-99e1-d10165884ca7").
+		Logger()
+	log.Err(errors.New("dial timeout")).
+		Msg("test message")
+
+	require.Nil(t, zerologError)
+	require.True(t, beforeSendCalled)
+}
+
+func TestWriteInvalidLevel(t *testing.T) {
+	cfg := Config{
+		ClientOptions: sentry.ClientOptions{
+			BeforeSend: func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event {
+				assert.Equal(t, sentry.LevelError, event.Level)
+				assert.Equal(t, "test message", event.Message)
+				require.Len(t, event.Exception, 1)
+				assert.Equal(t, "dial timeout", event.Exception[0].Value)
+				assert.True(t, time.Since(event.Timestamp).Minutes() < 1)
+				assert.Equal(t, "bee07485-2485-4f64-99e1-d10165884ca7", event.Extra["requestId"])
+				return event
+			},
+		},
+		Options: Options{
+			WithBreadcrumbs: true,
+		},
+	}
+	writer, err := New(cfg)
+	require.Nil(t, err)
+
+	log := zerolog.New(writer).With().Timestamp().
+		Str("requestId", "bee07485-2485-4f64-99e1-d10165884ca7").
+		Logger()
+	log.Log().Str("level", "invalid").Msg("test message")
+}
+
+func TestWrite_Disabled(t *testing.T) {
+	var beforeSendCalled bool
+	cfg := Config{
+		ClientOptions: sentry.ClientOptions{
+			BeforeSend: func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event {
+				beforeSendCalled = true
+				return event
+			},
+		},
+		Options: Options{
+			Levels:          []zerolog.Level{zerolog.FatalLevel},
+			WithBreadcrumbs: true,
+		},
+	}
+
+	writer, err := New(cfg)
+
+	require.Nil(t, err)
+
+	var zerologError error
+	zerolog.ErrorHandler = func(err error) {
+		zerologError = err
+	}
+
+	// use io.MultiWriter to enforce using the Write() method
+	log := zerolog.New(io.MultiWriter(writer)).With().Timestamp().
+		Str("requestId", "bee07485-2485-4f64-99e1-d10165884ca7").
+		Logger()
+	log.Err(errors.New("dial timeout")).
+		Msg("test message")
+
+	require.Nil(t, zerologError)
+	require.False(t, beforeSendCalled)
+}
+
+func TestWriteLevel_Disabled(t *testing.T) {
+	var beforeSendCalled bool
+	cfg := Config{
+		ClientOptions: sentry.ClientOptions{
+			BeforeSend: func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event {
+				beforeSendCalled = true
+				return event
+			},
+		},
+		Options: Options{
+			Levels:          []zerolog.Level{zerolog.FatalLevel},
+			WithBreadcrumbs: true,
+		},
+	}
+	writer, err := New(cfg)
+	require.Nil(t, err)
+
+	var zerologError error
+	zerolog.ErrorHandler = func(err error) {
+		zerologError = err
+	}
+
+	log := zerolog.New(writer).With().Timestamp().
+		Str("requestId", "bee07485-2485-4f64-99e1-d10165884ca7").
+		Str("go_version", "1.14").Str("go_max_procs", "4").
+		Logger()
+	log.Err(errors.New("dial timeout")).
+		Msg("test message")
+
+	require.Nil(t, zerologError)
+	require.False(t, beforeSendCalled)
+}
+
+func TestWriteLevelFatal(t *testing.T) {
+	var beforeSendCalled bool
+	cfg := Config{
+		ClientOptions: sentry.ClientOptions{
+			BeforeSend: func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event {
+				beforeSendCalled = true
+				return event
+			},
+		},
+		Options: Options{
+			Levels:          []zerolog.Level{zerolog.FatalLevel},
+			WithBreadcrumbs: true,
+		},
+	}
+	writer, err := New(cfg)
+	require.Nil(t, err)
+
+	var zerologError error
+	zerolog.ErrorHandler = func(err error) {
+		zerologError = err
+	}
+
+	logger := zerolog.New(writer).With().Timestamp().
+		Str("requestId", "bee07485-2485-4f64-99e1-d10165884ca7").
+		Str("go_version", "1.14").Str("go_max_procs", "4").Str("error", "dial timeout").Str("level", "fatal").Logger()
+
+	logger.Log().Msg("test message")
+
+	require.Nil(t, zerologError)
+	require.False(t, beforeSendCalled)
+}
+
+func BenchmarkParseLogEvent(b *testing.B) {
+	for i := 0; i < b.N; i++ {
+		parseLogEvent(logEventJSON)
+	}
+}
+
+func BenchmarkWriteLogEvent(b *testing.B) {
+	w, err := New(Config{})
+	if err != nil {
+		b.Errorf("failed to create writer: %v", err)
+	}
+
+	for i := 0; i < b.N; i++ {
+		_, _ = w.Write(logEventJSON)
+	}
+}
+
+func BenchmarkWriteLogLevelEvent(b *testing.B) {
+	w, err := New(Config{})
+	if err != nil {
+		b.Errorf("failed to create writer: %v", err)
+	}
+
+	for i := 0; i < b.N; i++ {
+		_, _ = w.WriteLevel(zerolog.ErrorLevel, logEventJSON)
+	}
+}

Description

This PR introduces several significant changes to the sentry-go SDK, including new integrations for zerolog and slog, updates to existing integrations, and various improvements and optimizations across the codebase. The main motivation appears to be expanding the SDK's compatibility with different logging libraries and improving its overall performance and functionality.

Possible Issues

  1. The removal of Go 1.18, 1.19, and 1.20 support might cause issues for projects still using these versions.
  2. Changes to the stacktrace.go file might affect error reporting and grouping in some edge cases.

Security Hotspots

  1. The fasthttp and fiber integrations now include additional error handling for parsing malformed URLs, which could potentially be exploited if not properly validated.
Changes

Changes

  1. .craft.yml:

    • Added new tag prefixes for slog and zerolog integrations.
  2. GitHub Workflows:

    • Updated Go version to 1.23 in lint workflow.
    • Removed testing for Go versions 1.18, 1.19, and 1.20.
    • Updated codecov action version.
  3. CHANGELOG.md:

    • Added entries for version 0.30.0, including new integrations and bug fixes.
  4. Makefile:

    • Updated mod-tidy and vet targets to use Go 1.21.
  5. README.md:

    • Added mentions of new integrations (fiber, logrus, slog, zerolog).
  6. New integrations:

    • Added slog integration for structured logging.
    • Added zerolog integration for zero-allocation logging.
  7. Existing integrations:

    • Updated fasthttp and fiber integrations to handle malformed URLs more gracefully.
  8. Core SDK changes:

    • Removed version-specific Go files and consolidated logic.
    • Optimized various functions using new Go 1.21 features (e.g., slices package).
    • Updated minimum Go version to 1.21 in go.mod.
  9. Error handling:

    • Modified exception handling to always set Mechanism Type to "generic".
  10. Testing:

    • Updated and added new tests for the changes made.
sequenceDiagram
    participant App as Application
    participant SDK as Sentry SDK
    participant Logger as Logger (slog/zerolog)
    participant Sentry as Sentry Backend

    App->>SDK: Initialize SDK
    App->>Logger: Configure Logger with Sentry integration
    App->>Logger: Log message/error
    Logger->>SDK: Pass log entry
    SDK->>SDK: Process log entry
    alt Is error or configured level
        SDK->>Sentry: Send event
    else Is lower level and breadcrumbs enabled
        SDK->>SDK: Add as breadcrumb
    end
    App->>SDK: Flush before exit
    SDK->>Sentry: Send any remaining events
Loading

@renovate renovate bot changed the title fix(deps): update module github.com/getsentry/sentry-go to v0.30.0 fix(deps): update module github.com/getsentry/sentry-go to v0.31.1 Jan 9, 2025
@renovate renovate bot force-pushed the renovate/github.com-getsentry-sentry-go-0.x branch from 9856617 to 65989c7 Compare January 9, 2025 16:01
@renovate renovate bot force-pushed the renovate/github.com-getsentry-sentry-go-0.x branch from 65989c7 to 877e6ca Compare January 23, 2025 19:03
@renovate renovate bot changed the title fix(deps): update module github.com/getsentry/sentry-go to v0.31.1 fix(deps): update module github.com/getsentry/sentry-go to v0.31.1 - autoclosed Jan 27, 2025
@renovate renovate bot closed this Jan 27, 2025
@renovate renovate bot deleted the renovate/github.com-getsentry-sentry-go-0.x branch January 27, 2025 00:22
@renovate renovate bot changed the title fix(deps): update module github.com/getsentry/sentry-go to v0.31.1 - autoclosed fix(deps): update module github.com/getsentry/sentry-go to v0.31.1 Jan 29, 2025
@renovate renovate bot reopened this Jan 29, 2025
@renovate renovate bot force-pushed the renovate/github.com-getsentry-sentry-go-0.x branch 2 times, most recently from 877e6ca to 1633ad0 Compare January 29, 2025 09:25
@renovate renovate bot force-pushed the renovate/github.com-getsentry-sentry-go-0.x branch 3 times, most recently from 42837bf to 55e541d Compare February 6, 2025 10:24
@renovate renovate bot force-pushed the renovate/github.com-getsentry-sentry-go-0.x branch 3 times, most recently from 3b3b323 to eeb45a8 Compare March 5, 2025 15:41
@renovate renovate bot force-pushed the renovate/github.com-getsentry-sentry-go-0.x branch from eeb45a8 to 531127a Compare March 24, 2025 16:29
Copy link

socket-security bot commented Mar 24, 2025

No dependency changes detected. Learn more about Socket for GitHub ↗︎

👍 No dependency changes detected in pull request

@renovate renovate bot force-pushed the renovate/github.com-getsentry-sentry-go-0.x branch from 531127a to c719589 Compare March 28, 2025 01:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

0 participants