upgrade deps; rewrite smtp session

This commit is contained in:
Aine 2024-02-19 22:55:14 +02:00
parent 10213cc7d7
commit a01720da00
No known key found for this signature in database
GPG key ID: 34969C908CCA2804
277 changed files with 106832 additions and 7641 deletions

View file

@ -44,10 +44,10 @@ func eventToContext(ctx context.Context, evt *event.Event) context.Context {
ctx = context.WithValue(ctx, ctxEvent, evt) ctx = context.WithValue(ctx, ctxEvent, evt)
sentry.GetHubFromContext(ctx).ConfigureScope(func(scope *sentry.Scope) { sentry.GetHubFromContext(ctx).ConfigureScope(func(scope *sentry.Scope) {
scope.SetUser(sentry.User{ID: evt.Sender.String()}) scope.SetUser(sentry.User{ID: evt.Sender.String()})
scope.SetContext("event", map[string]string{ scope.SetContext("event", map[string]any{
"id": evt.ID.String(), "id": evt.ID,
"room": evt.RoomID.String(), "room": evt.RoomID,
"sender": evt.Sender.String(), "sender": evt.Sender,
}) })
}) })

42
go.mod
View file

@ -7,17 +7,17 @@ toolchain go1.22.0
// replace gitlab.com/etke.cc/linkpearl => ../linkpearl // replace gitlab.com/etke.cc/linkpearl => ../linkpearl
require ( require (
github.com/archdx/zerolog-sentry v1.2.0 github.com/archdx/zerolog-sentry v1.8.2
github.com/emersion/go-msgauth v0.6.6 github.com/emersion/go-msgauth v0.6.8
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43
github.com/emersion/go-smtp v0.15.0 github.com/emersion/go-smtp v0.20.2
github.com/fsnotify/fsnotify v1.6.0 github.com/fsnotify/fsnotify v1.7.0
github.com/gabriel-vasile/mimetype v1.4.1 github.com/gabriel-vasile/mimetype v1.4.3
github.com/getsentry/sentry-go v0.13.0 github.com/getsentry/sentry-go v0.27.0
github.com/jhillyerd/enmime v0.10.0 github.com/jhillyerd/enmime v1.2.0
github.com/lib/pq v1.10.9 github.com/lib/pq v1.10.9
github.com/mattn/go-sqlite3 v1.14.19 github.com/mattn/go-sqlite3 v1.14.22
github.com/mcnijman/go-emailaddress v1.1.0 github.com/mcnijman/go-emailaddress v1.1.1
github.com/mileusna/crontab v1.2.0 github.com/mileusna/crontab v1.2.0
github.com/raja/argon2pw v1.0.2-0.20210910183755-a391af63bd39 github.com/raja/argon2pw v1.0.2-0.20210910183755-a391af63bd39
github.com/rs/zerolog v1.32.0 github.com/rs/zerolog v1.32.0
@ -26,36 +26,36 @@ require (
gitlab.com/etke.cc/go/healthchecks v1.0.1 gitlab.com/etke.cc/go/healthchecks v1.0.1
gitlab.com/etke.cc/go/mxidwc v1.0.0 gitlab.com/etke.cc/go/mxidwc v1.0.0
gitlab.com/etke.cc/go/psd v1.0.0 gitlab.com/etke.cc/go/psd v1.0.0
gitlab.com/etke.cc/go/secgen v1.1.1 gitlab.com/etke.cc/go/secgen v1.2.0
gitlab.com/etke.cc/go/validator v1.0.6 gitlab.com/etke.cc/go/validator v1.0.7
gitlab.com/etke.cc/linkpearl v0.0.0-20240211143445-bddf907d137a gitlab.com/etke.cc/linkpearl v0.0.0-20240211143445-bddf907d137a
golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3 golang.org/x/exp v0.0.0-20240213143201-ec583247a57a
maunium.net/go/mautrix v0.17.0 maunium.net/go/mautrix v0.17.0
) )
require ( require (
blitiri.com.ar/go/spf v1.5.1 // indirect blitiri.com.ar/go/spf v1.5.1 // indirect
github.com/buger/jsonparser v1.0.0 // indirect github.com/buger/jsonparser v1.1.1 // indirect
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect
github.com/gogs/chardet v0.0.0-20191104214054-4b6791f73a28 // indirect github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f // indirect
github.com/google/uuid v1.3.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7 // indirect github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.12 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a // indirect github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/rivo/uniseg v0.2.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/tidwall/gjson v1.17.0 // indirect github.com/tidwall/gjson v1.17.1 // indirect
github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect github.com/tidwall/sjson v1.2.5 // indirect
github.com/yuin/goldmark v1.7.0 // indirect github.com/yuin/goldmark v1.7.0 // indirect
gitlab.com/etke.cc/go/trysmtp v1.1.3 // indirect gitlab.com/etke.cc/go/trysmtp v1.1.3 // indirect
go.mau.fi/util v0.3.0 // indirect go.mau.fi/util v0.4.0 // indirect
golang.org/x/crypto v0.19.0 // indirect golang.org/x/crypto v0.19.0 // indirect
golang.org/x/net v0.21.0 // indirect golang.org/x/net v0.21.0 // indirect
golang.org/x/sys v0.17.0 // indirect golang.org/x/sys v0.17.0 // indirect

125
go.sum
View file

@ -1,54 +1,48 @@
blitiri.com.ar/go/spf v1.5.1 h1:CWUEasc44OrANJD8CzceRnRn1Jv0LttY68cYym2/pbE= blitiri.com.ar/go/spf v1.5.1 h1:CWUEasc44OrANJD8CzceRnRn1Jv0LttY68cYym2/pbE=
blitiri.com.ar/go/spf v1.5.1/go.mod h1:E71N92TfL4+Yyd5lpKuE9CAF2pd4JrUq1xQfkTxoNdk= blitiri.com.ar/go/spf v1.5.1/go.mod h1:E71N92TfL4+Yyd5lpKuE9CAF2pd4JrUq1xQfkTxoNdk=
github.com/DATA-DOG/go-sqlmock v1.5.1 h1:FK6RCIUSfmbnI/imIICmboyQBkOckutaa6R5YYlLZyo= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.1/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/archdx/zerolog-sentry v1.2.0 h1:FDFqlo5XvL/jpDAPoAWI15EjJQVFvixn70v3IH//eTM= github.com/archdx/zerolog-sentry v1.8.2 h1:zS8n0+H7SsG161RN8dP47CSsdyrhODdo9LEDOPXJhXI=
github.com/archdx/zerolog-sentry v1.2.0/go.mod h1:3H8gClGFafB90fKMsvfP017bdmkG5MD6UiA+6iPEwGw= github.com/archdx/zerolog-sentry v1.8.2/go.mod h1:XrFHGe1CH5DQk/XSySu/IJSi5C9XR6+zpc97zVf/c4c=
github.com/buger/jsonparser v1.0.0 h1:etJTGF5ESxjI0Ic2UaLQs2LQQpa8G9ykQScukbh4L8A= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.0.0/go.mod h1:tgcrVJ81GPSF0mz+0nu1Xaz0fazGPrmmJfJtxjbHhUQ= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI= github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI=
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8= github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 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/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emersion/go-message v0.11.2/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY= github.com/emersion/go-msgauth v0.6.8 h1:kW/0E9E8Zx5CdKsERC/WnAvnXvX7q9wTHia1OA4944A=
github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4= github.com/emersion/go-msgauth v0.6.8/go.mod h1:YDwuyTCUHu9xxmAeVj0eW4INnwB6NNZoPdLerpSxRrc=
github.com/emersion/go-milter v0.3.3/go.mod h1:ablHK0pbLB83kMFBznp/Rj8aV+Kc3jw8cxzzmCNLIOY=
github.com/emersion/go-msgauth v0.6.6 h1:buv5lL8v/3v4RpHnQFS2IPhE3nxSRX+AxnrEJbDbHhA=
github.com/emersion/go-msgauth v0.6.6/go.mod h1:A+/zaz9bzukLM6tRWRgJ3BdrBi+TFKTvQ3fGMFOI9SM=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-smtp v0.15.0 h1:3+hMGMGrqP/lqd7qoxZc1hTU8LY8gHV9RFGWlqSDmP8= github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 h1:hH4PQfOndHDlpzYfLAAfl63E8Le6F2+EL/cdhlkyRJY=
github.com/emersion/go-smtp v0.15.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ= github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= github.com/emersion/go-smtp v0.20.2 h1:peX42Qnh5Q0q3vrAnRy43R/JwTnnv75AebxbkTL7Ia4=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= github.com/emersion/go-smtp v0.20.2/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gabriel-vasile/mimetype v1.4.1 h1:TRWk7se+TOjCYgRth7+1/OYLNiRNIotknkFtf/dnN7Q= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.1/go.mod h1:05Vi0w3Y9c/lNvJOdmIwvrrAhX3rYhfQQCaf9VJcv7M= github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/getsentry/sentry-go v0.13.0 h1:20dgTiUSfxRB/EhMPtxcL9ZEbM1ZdR+W/7f7NWD+xWo= github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps=
github.com/getsentry/sentry-go v0.13.0/go.mod h1:EOsfu5ZdvKPfeHYV6pTVQnsjfp30+XA7//UooKNumH0= github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-test/deep v1.0.7 h1:/VSMRlnY/JSyqxQUzQLKVMAskpY/NZKFA5j2P+0pP2M= github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
github.com/go-test/deep v1.0.7/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8= github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogs/chardet v0.0.0-20191104214054-4b6791f73a28 h1:gBeyun7mySAKWg7Fb0GOcv0upX9bdaZScs8QcRo8mEY= github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f h1:3BSP1Tbs2djlpprl7wCLuiqMaUh5SJkkzI2gDs+FgLs=
github.com/gogs/chardet v0.0.0-20191104214054-4b6791f73a28/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14= github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7 h1:g0fAGBisHaEQ0TRq1iBvemFRf+8AEWEmBESSiWB3Vsc= github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 h1:iCHtR9CQyktQ5+f3dMVZfwD2KWJUgm7M0gdL9NGr8KA=
github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk= github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
github.com/jhillyerd/enmime v0.10.0 h1:DZEzhptPRBesvN3gf7K1BOh4rfpqdsdrEoxW1Edr/3s= github.com/jhillyerd/enmime v1.2.0 h1:dIu1IPEymQgoT2dzuB//ttA/xcV40NMPpQtmd4wslHk=
github.com/jhillyerd/enmime v0.10.0/go.mod h1:Qpe8EEemJMFAF8+NZoWdpXvK2Yb9dRF0k/z6mkcDHsA= github.com/jhillyerd/enmime v1.2.0/go.mod h1:FRFuUPCLh8PByQv+8xRcLO9QHqaqTqreYhopv5eyk4I=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/martinlindhe/base36 v1.0.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 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-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.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
@ -56,12 +50,12 @@ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 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/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.12 h1:Y41i/hVW3Pgwr8gV+J23B9YEY0zxjptBuCWEaxmAOow= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mcnijman/go-emailaddress v1.1.0 h1:7/Uxgn9pXwXmvXsFSgORo6XoRTrttj7AGmmB2yFArAg= github.com/mcnijman/go-emailaddress v1.1.1 h1:AGhgVDG3tCDaL0/Vc6erlPQjDuDN3dAT7rRdgFtetr0=
github.com/mcnijman/go-emailaddress v1.1.0/go.mod h1:m+aauxGmv31sB5zZ1I8ICcMoa9ZHOA9RiurCijfvkhI= github.com/mcnijman/go-emailaddress v1.1.1/go.mod h1:5whZrhS8Xp5LxO8zOD35BC+b76kROtsh+dPomeRt/II=
github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a h1:eU8j/ClY2Ty3qdHnn0TyW3ivFoPC/0F1gQZz8yTxbbE= github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a h1:eU8j/ClY2Ty3qdHnn0TyW3ivFoPC/0F1gQZz8yTxbbE=
github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a/go.mod h1:v8eSC2SMp9/7FTKUncp7fH9IwPfw+ysMObcEz5FWheQ= github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a/go.mod h1:v8eSC2SMp9/7FTKUncp7fH9IwPfw+ysMObcEz5FWheQ=
github.com/mileusna/crontab v1.2.0 h1:x9ZmE2A4p6CDqMEGQ+GbqsNtnmbdmWMQYShdQu8LvrU= github.com/mileusna/crontab v1.2.0 h1:x9ZmE2A4p6CDqMEGQ+GbqsNtnmbdmWMQYShdQu8LvrU=
@ -76,22 +70,19 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/raja/argon2pw v1.0.2-0.20210910183755-a391af63bd39 h1:2by0+lF6NfaNWhlpsv1DfBQzwbAyYUPIsMWYapek/Sk= github.com/raja/argon2pw v1.0.2-0.20210910183755-a391af63bd39 h1:2by0+lF6NfaNWhlpsv1DfBQzwbAyYUPIsMWYapek/Sk=
github.com/raja/argon2pw v1.0.2-0.20210910183755-a391af63bd39/go.mod h1:idX/fPqwjX31YMTF2iIpEpNApV2YbQhSFr4iIhJaqp4= github.com/raja/argon2pw v1.0.2-0.20210910183755-a391af63bd39/go.mod h1:idX/fPqwjX31YMTF2iIpEpNApV2YbQhSFr4iIhJaqp4=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0= github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0=
github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo= github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM= github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM= github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U=
github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
@ -111,49 +102,33 @@ gitlab.com/etke.cc/go/mxidwc v1.0.0 h1:6EAlJXvs3nU4RaMegYq6iFlyVvLw7JZYnZmNCGMYQ
gitlab.com/etke.cc/go/mxidwc v1.0.0/go.mod h1:E/0kh45SAN9+ntTG0cwkAEKdaPxzvxVmnjwivm9nmz8= gitlab.com/etke.cc/go/mxidwc v1.0.0/go.mod h1:E/0kh45SAN9+ntTG0cwkAEKdaPxzvxVmnjwivm9nmz8=
gitlab.com/etke.cc/go/psd v1.0.0 h1:ucr0/1Qq92hFT/mjqr/k3rHaVpPHEDTRbX6koTKon7Y= gitlab.com/etke.cc/go/psd v1.0.0 h1:ucr0/1Qq92hFT/mjqr/k3rHaVpPHEDTRbX6koTKon7Y=
gitlab.com/etke.cc/go/psd v1.0.0/go.mod h1:6b444NOkXlZ1n7WLCNazAkOC2bHPgqgfB9earThwKPk= gitlab.com/etke.cc/go/psd v1.0.0/go.mod h1:6b444NOkXlZ1n7WLCNazAkOC2bHPgqgfB9earThwKPk=
gitlab.com/etke.cc/go/secgen v1.1.1 h1:RmKOki725HIhWJHzPtAc9X4YvBneczndchpMgoDkE8w= gitlab.com/etke.cc/go/secgen v1.2.0 h1:qpV7rUn5Rs6eWxAmbGG/idPCOgsN4HggGmSZ+1R/L70=
gitlab.com/etke.cc/go/secgen v1.1.1/go.mod h1:3pJqRGeWApzx7qXjABqz2o2SMCNpKSZao/gXVdasqE8= gitlab.com/etke.cc/go/secgen v1.2.0/go.mod h1:v5L07AIXtNpC/miYiK0TMIn+ZKbiYrTRiXTw6qTL6pw=
gitlab.com/etke.cc/go/trysmtp v1.1.3 h1:e2EHond77onMaecqCg6mWumffTSEf+ycgj88nbeefDI= gitlab.com/etke.cc/go/trysmtp v1.1.3 h1:e2EHond77onMaecqCg6mWumffTSEf+ycgj88nbeefDI=
gitlab.com/etke.cc/go/trysmtp v1.1.3/go.mod h1:lOO7tTdAE0a3ETV3wN3GJ7I1Tqewu7YTpPWaOmTteV0= gitlab.com/etke.cc/go/trysmtp v1.1.3/go.mod h1:lOO7tTdAE0a3ETV3wN3GJ7I1Tqewu7YTpPWaOmTteV0=
gitlab.com/etke.cc/go/validator v1.0.6 h1:w0Muxf9Pqw7xvF7NaaswE6d7r9U3nB2t2l5PnFMrecQ= gitlab.com/etke.cc/go/validator v1.0.7 h1:4BGDTa9x68vJhbyn7m8W2yX+2Nb5im9+JLRrgoLUlF4=
gitlab.com/etke.cc/go/validator v1.0.6/go.mod h1:Id0SxRj0J3IPhiKlj0w1plxVLZfHlkwipn7HfRZsDts= gitlab.com/etke.cc/go/validator v1.0.7/go.mod h1:Id0SxRj0J3IPhiKlj0w1plxVLZfHlkwipn7HfRZsDts=
gitlab.com/etke.cc/linkpearl v0.0.0-20240211143445-bddf907d137a h1:30WtX+uepGqyFnU7jIockJWxQUeYdljhhk63DCOXLZs= gitlab.com/etke.cc/linkpearl v0.0.0-20240211143445-bddf907d137a h1:30WtX+uepGqyFnU7jIockJWxQUeYdljhhk63DCOXLZs=
gitlab.com/etke.cc/linkpearl v0.0.0-20240211143445-bddf907d137a/go.mod h1:3lqQGDDtk52Jm8PD3mZ3qhmIp4JXuq95waWH5vmEacc= gitlab.com/etke.cc/linkpearl v0.0.0-20240211143445-bddf907d137a/go.mod h1:3lqQGDDtk52Jm8PD3mZ3qhmIp4JXuq95waWH5vmEacc=
go.mau.fi/util v0.3.0 h1:Lt3lbRXP6ZBqTINK0EieRWor3zEwwwrDT14Z5N8RUCs= go.mau.fi/util v0.4.0 h1:S2X3qU4pUcb/vxBRfAuZjbrR9xVMAXSjQojNBLPBbhs=
go.mau.fi/util v0.3.0/go.mod h1:9dGsBCCbZJstx16YgnVMVi3O2bOizELoKpugLD4FoGs= go.mau.fi/util v0.4.0/go.mod h1:leeiHtgVBuN+W9aDii3deAXnfC563iN3WK6BF8/AjNw=
golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3 h1:/RIbNt/Zr7rVhIkQhooTxCxFcdWLGIKnZA4IXNFSrvo= golang.org/x/exp v0.0.0-20240213143201-ec583247a57a h1:HinSgX1tJRX3KsL//Gxynpw5CTOAIPhgL4W8PNiIpVE=
golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= golang.org/x/exp v0.0.0-20240213143201-ec583247a57a/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20210501142056-aec3718b3fa0/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/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.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
maunium.net/go/maulogger/v2 v2.4.1 h1:N7zSdd0mZkB2m2JtFUsiGTQQAdP0YeFWT7YMc80yAL8= maunium.net/go/maulogger/v2 v2.4.1 h1:N7zSdd0mZkB2m2JtFUsiGTQQAdP0YeFWT7YMc80yAL8=

View file

@ -93,7 +93,7 @@ func NewManager(cfg *Config) *Manager {
s.ErrorLog = loggerWrapper{func(s string, i ...any) { cfg.Logger.Error().Msgf(s, i...) }} s.ErrorLog = loggerWrapper{func(s string, i ...any) { cfg.Logger.Error().Msgf(s, i...) }}
s.ReadTimeout = 10 * time.Second s.ReadTimeout = 10 * time.Second
s.WriteTimeout = 10 * time.Second s.WriteTimeout = 10 * time.Second
s.MaxMessageBytes = cfg.MaxSize * 1024 * 1024 s.MaxMessageBytes = int64(cfg.MaxSize * 1024 * 1024)
s.AllowInsecureAuth = !cfg.TLSRequired s.AllowInsecureAuth = !cfg.TLSRequired
s.EnableREQUIRETLS = cfg.TLSRequired s.EnableREQUIRETLS = cfg.TLSRequired
s.EnableSMTPUTF8 = true s.EnableSMTPUTF8 = true

View file

@ -43,60 +43,16 @@ type mailServer struct {
sender MailSender sender MailSender
} }
// Login used for outgoing mail submissions only (when you use postmoogle as smtp server in your scripts) func (m *mailServer) NewSession(con *smtp.Conn) (smtp.Session, error) {
func (m *mailServer) Login(state *smtp.ConnectionState, username, password string) (smtp.Session, error) { ctx := sentry.SetHubOnContext(context.Background(), sentry.CurrentHub().Clone())
m.log.Debug().Str("username", username).Any("state", state).Msg("Login") return &session{
ctx := context.Background() log: m.log,
if m.bot.IsBanned(ctx, state.RemoteAddr) { bot: m.bot,
return nil, ErrBanned domains: m.domains,
} sendmail: m.sender.Send,
conn: con,
if !email.AddressValid(username) { ctx: ctx,
m.log.Debug().Str("address", username).Msg("address is invalid") privkey: m.bot.GetDKIMprivkey(ctx),
m.bot.BanAuth(ctx, state.RemoteAddr)
return nil, ErrBanned
}
roomID, allow := m.bot.AllowAuth(ctx, username, password)
if !allow {
m.log.Debug().Str("username", username).Msg("username or password is invalid")
m.bot.BanAuth(ctx, state.RemoteAddr)
return nil, ErrBanned
}
return &outgoingSession{
ctx: sentry.SetHubOnContext(context.Background(), sentry.CurrentHub().Clone()),
sendmail: m.sender.Send,
privkey: m.bot.GetDKIMprivkey(ctx),
from: username,
log: m.log,
domains: m.domains,
getRoomID: m.bot.GetMapping,
fromRoom: roomID,
tos: []string{},
}, nil
}
// AnonymousLogin used for incoming mail submissions only
func (m *mailServer) AnonymousLogin(state *smtp.ConnectionState) (smtp.Session, error) {
m.log.Debug().Any("state", state).Msg("AnonymousLogin")
ctx := context.Background()
if m.bot.IsBanned(ctx, state.RemoteAddr) {
return nil, ErrBanned
}
return &incomingSession{
ctx: sentry.SetHubOnContext(context.Background(), sentry.CurrentHub().Clone()),
getRoomID: m.bot.GetMapping,
getFilters: m.bot.GetIFOptions,
receiveEmail: m.ReceiveEmail,
ban: m.bot.BanAuto,
greylisted: m.bot.IsGreylisted,
trusted: m.bot.IsTrusted,
log: m.log,
domains: m.domains,
addr: state.RemoteAddr,
tos: []string{},
}, nil }, nil
} }

View file

@ -10,18 +10,22 @@ import (
"github.com/emersion/go-msgauth/dkim" "github.com/emersion/go-msgauth/dkim"
"github.com/emersion/go-smtp" "github.com/emersion/go-smtp"
"github.com/getsentry/sentry-go"
"github.com/jhillyerd/enmime" "github.com/jhillyerd/enmime"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"gitlab.com/etke.cc/go/validator" "gitlab.com/etke.cc/go/validator"
"maunium.net/go/mautrix/id"
"gitlab.com/etke.cc/postmoogle/email" "gitlab.com/etke.cc/postmoogle/email"
"gitlab.com/etke.cc/postmoogle/utils" "gitlab.com/etke.cc/postmoogle/utils"
"maunium.net/go/mautrix/id"
) )
// GraylistCode SMTP code const (
const GraylistCode = 451 // GraylistCode SMTP code
GraylistCode = 451
// Incoming is the direction of the email
Incoming = "incoming"
// Outgoing is the direction of the email
Outoing = "outgoing"
)
var ( var (
// ErrInvalidEmail for invalid emails :) // ErrInvalidEmail for invalid emails :)
@ -30,89 +34,106 @@ var (
GraylistEnhancedCode = smtp.EnhancedCode{4, 5, 1} GraylistEnhancedCode = smtp.EnhancedCode{4, 5, 1}
) )
// incomingSession represents an SMTP-submission session receiving emails from remote servers type session struct {
type incomingSession struct { log *zerolog.Logger
log *zerolog.Logger bot matrixbot
getRoomID func(context.Context, string) (id.RoomID, bool) ctx context.Context //nolint:containedctx // that's session
getFilters func(context.Context, id.RoomID) email.IncomingFilteringOptions conn *smtp.Conn
receiveEmail func(context.Context, *email.Email) error domains []string
greylisted func(context.Context, net.Addr) bool sendmail func(string, string, string) error
trusted func(net.Addr) bool
ban func(context.Context, net.Addr)
domains []string
roomID id.RoomID
ctx context.Context //nolint:containedctx // that's session dir string
addr net.Addr tos []string
tos []string from string
from string roomID id.RoomID
privkey string
fromRoom id.RoomID
} }
func (s *incomingSession) Mail(from string, opts smtp.MailOptions) error { func (s *session) AuthPlain(username, password string) error {
sentry.GetHubFromContext(s.ctx).Scope().SetTag("from", from) addr := s.conn.Conn().RemoteAddr()
if !email.AddressValid(from) { if s.bot.IsBanned(s.ctx, addr) {
s.log.Debug().Str("from", from).Msg("address is invalid")
s.ban(s.ctx, s.addr)
return ErrBanned return ErrBanned
} }
s.from = email.Address(from) if !email.AddressValid(username) {
s.log.Debug().Str("from", from).Any("options", opts).Msg("incoming mail") s.log.Debug().Str("address", username).Msg("address is invalid")
s.bot.BanAuth(s.ctx, addr)
return ErrBanned
}
roomID, allow := s.bot.AllowAuth(s.ctx, username, password)
if !allow {
s.log.Debug().Str("username", username).Msg("username or password is invalid")
s.bot.BanAuth(s.ctx, addr)
return ErrBanned
}
s.dir = Outoing
s.from = username
s.fromRoom = roomID
return nil return nil
} }
func (s *incomingSession) Rcpt(to string) error { func (s *session) Mail(from string, _ *smtp.MailOptions) error {
sentry.GetHubFromContext(s.ctx).Scope().SetTag("to", to) if s.dir == Outoing {
if err := s.validateOutgoingMail(from); err != nil {
return err
}
} else {
if !email.AddressValid(from) {
s.log.Debug().Str("from", from).Msg("address is invalid")
s.bot.BanAuto(s.ctx, s.conn.Conn().RemoteAddr())
return ErrBanned
}
s.from = email.Address(from)
s.log.Debug().Str("from", from).Msg("incoming mail")
}
return nil
}
func (s *session) Rcpt(to string, _ *smtp.RcptOptions) error {
s.tos = append(s.tos, to) s.tos = append(s.tos, to)
hostname := utils.Hostname(to) s.log.Debug().Str("to", to).Msg("mail")
var domainok bool if s.dir != Outoing {
for _, domain := range s.domains { if err := s.validateIncomingRcpt(to); err != nil {
if hostname == domain { return err
domainok = true
break
} }
} }
if !domainok {
s.log.Debug().Str("to", to).Msg("wrong domain")
return ErrNoUser
}
var ok bool
s.roomID, ok = s.getRoomID(s.ctx, utils.Mailbox(to))
if !ok {
s.log.Debug().Str("to", to).Msg("mapping not found")
return ErrNoUser
}
s.log.Debug().Str("to", to).Msg("mail")
return nil return nil
} }
// getAddr gets real address of incoming email serder, func (s *session) Data(r io.Reader) error {
// including special case of trusted proxy if s.dir == Outoing {
func (s *incomingSession) getAddr(envelope *enmime.Envelope) net.Addr { return s.outgoingData(r)
if !s.trusted(s.addr) {
return s.addr
} }
return s.incomingData(r)
addrHeader := envelope.GetHeader("X-Real-Addr")
if addrHeader == "" {
return s.addr
}
host, portString, _ := net.SplitHostPort(addrHeader) //nolint:errcheck // it is real addr
if host == "" {
return s.addr
}
var port int
port, _ = strconv.Atoi(portString) //nolint:errcheck // it's a real addr
realAddr := &net.TCPAddr{IP: net.ParseIP(host), Port: port}
s.log.Info().Str("addr", realAddr.String()).Msg("real address")
return realAddr
} }
func (s *incomingSession) Data(r io.Reader) error { func (s *session) Reset() {}
func (s *session) Logout() error {
return nil
}
func (s *session) outgoingData(r io.Reader) error {
parser := enmime.NewParser()
envelope, err := parser.ReadEnvelope(r)
if err != nil {
return err
}
eml := email.FromEnvelope(s.tos[0], envelope)
for _, to := range s.tos {
eml.RcptTo = to
err := s.sendmail(eml.From, to, eml.Compose(s.privkey))
if err != nil {
return err
}
}
return nil
}
func (s *session) incomingData(r io.Reader) error {
data, err := io.ReadAll(r) data, err := io.ReadAll(r)
if err != nil { if err != nil {
s.log.Error().Err(err).Msg("cannot read DATA") s.log.Error().Err(err).Msg("cannot read DATA")
@ -126,12 +147,12 @@ func (s *incomingSession) Data(r io.Reader) error {
} }
addr := s.getAddr(envelope) addr := s.getAddr(envelope)
reader.Seek(0, io.SeekStart) //nolint:errcheck // becase we're sure that's ok reader.Seek(0, io.SeekStart) //nolint:errcheck // becase we're sure that's ok
validations := s.getFilters(s.ctx, s.roomID) validations := s.bot.GetIFOptions(s.ctx, s.roomID)
if !validateIncoming(s.from, s.tos[0], addr, s.log, validations) { if !validateIncoming(s.from, s.tos[0], addr, s.log, validations) {
s.ban(s.ctx, addr) s.bot.BanAuth(s.ctx, addr)
return ErrBanned return ErrBanned
} }
if s.greylisted(s.ctx, addr) { if s.bot.IsGreylisted(s.ctx, addr) {
return &smtp.SMTPError{ return &smtp.SMTPError{
Code: GraylistCode, Code: GraylistCode,
EnhancedCode: GraylistEnhancedCode, EnhancedCode: GraylistEnhancedCode,
@ -155,7 +176,7 @@ func (s *incomingSession) Data(r io.Reader) error {
eml := email.FromEnvelope(s.tos[0], envelope) eml := email.FromEnvelope(s.tos[0], envelope)
for _, to := range s.tos { for _, to := range s.tos {
eml.RcptTo = to eml.RcptTo = to
err := s.receiveEmail(s.ctx, eml) err := s.bot.IncomingEmail(s.ctx, eml)
if err != nil { if err != nil {
return err return err
} }
@ -163,25 +184,8 @@ func (s *incomingSession) Data(r io.Reader) error {
return nil return nil
} }
func (s *incomingSession) Reset() {} // validateOutgoingMail checks if the sender is allowed to send mail
func (s *incomingSession) Logout() error { return nil } func (s *session) validateOutgoingMail(from string) error {
// outgoingSession represents an SMTP-submission session sending emails from external scripts, using postmoogle as SMTP server
type outgoingSession struct {
log *zerolog.Logger
sendmail func(string, string, string) error
privkey string
domains []string
getRoomID func(context.Context, string) (id.RoomID, bool)
ctx context.Context //nolint:containedctx // that's session
tos []string
from string
fromRoom id.RoomID
}
func (s *outgoingSession) Mail(from string, _ smtp.MailOptions) error {
sentry.GetHubFromContext(s.ctx).Scope().SetTag("from", from)
if !email.AddressValid(from) { if !email.AddressValid(from) {
return ErrInvalidEmail return ErrInvalidEmail
} }
@ -198,7 +202,7 @@ func (s *outgoingSession) Mail(from string, _ smtp.MailOptions) error {
return ErrNoUser return ErrNoUser
} }
roomID, ok := s.getRoomID(s.ctx, utils.Mailbox(from)) roomID, ok := s.bot.GetMapping(s.ctx, utils.Mailbox(from))
if !ok { if !ok {
s.log.Debug().Str("from", from).Msg("mapping not found") s.log.Debug().Str("from", from).Msg("mapping not found")
return ErrNoUser return ErrNoUser
@ -210,33 +214,56 @@ func (s *outgoingSession) Mail(from string, _ smtp.MailOptions) error {
return nil return nil
} }
func (s *outgoingSession) Rcpt(to string) error { // validateIncomingRcpt checks if the recipient is allowed to receive mail
sentry.GetHubFromContext(s.ctx).Scope().SetTag("to", to) func (s *session) validateIncomingRcpt(to string) error {
s.tos = append(s.tos, to) hostname := utils.Hostname(to)
var domainok bool
for _, domain := range s.domains {
if hostname == domain {
domainok = true
break
}
}
if !domainok {
s.log.Debug().Str("to", to).Msg("wrong domain")
return ErrNoUser
}
var ok bool
s.roomID, ok = s.bot.GetMapping(s.ctx, utils.Mailbox(to))
if !ok {
s.log.Debug().Str("to", to).Msg("mapping not found")
return ErrNoUser
}
s.log.Debug().Str("to", to).Msg("mail") s.log.Debug().Str("to", to).Msg("mail")
return nil return nil
} }
func (s *outgoingSession) Data(r io.Reader) error { // getAddr gets real address of incoming email serder,
parser := enmime.NewParser() // including special case of trusted proxy
envelope, err := parser.ReadEnvelope(r) func (s *session) getAddr(envelope *enmime.Envelope) net.Addr {
if err != nil { remoteAddr := s.conn.Conn().RemoteAddr()
return err if !s.bot.IsTrusted(remoteAddr) {
} return remoteAddr
eml := email.FromEnvelope(s.tos[0], envelope)
for _, to := range s.tos {
eml.RcptTo = to
err := s.sendmail(eml.From, to, eml.Compose(s.privkey))
if err != nil {
return err
}
} }
return nil addrHeader := envelope.GetHeader("X-Real-Addr")
if addrHeader == "" {
return remoteAddr
}
host, portString, _ := net.SplitHostPort(addrHeader) //nolint:errcheck // it is real addr
if host == "" {
return remoteAddr
}
var port int
port, _ = strconv.Atoi(portString) //nolint:errcheck // it's a real addr
realAddr := &net.TCPAddr{IP: net.ParseIP(host), Port: port}
s.log.Info().Str("addr", realAddr.String()).Msg("real address")
return realAddr
} }
func (s *outgoingSession) Reset() {}
func (s *outgoingSession) Logout() error { return nil }
func validateIncoming(from, to string, senderAddr net.Addr, log *zerolog.Logger, options email.IncomingFilteringOptions) bool { func validateIncoming(from, to string, senderAddr net.Addr, log *zerolog.Logger, options email.IncomingFilteringOptions) bool {
var sender net.IP var sender net.IP

View file

@ -6,7 +6,6 @@
```go ```go
import ( import (
"errors" "errors"
"io"
stdlog "log" stdlog "log"
"os" "os"
@ -15,14 +14,15 @@ import (
) )
func main() { func main() {
w, err := zlogsentry.New("http://e35657dcf4fb4d7c98a1c0b8a9125088@localhost:9000/2") w, err := zlogsentry.New("http://e35657dcf4fb4d7c98a1c0b8a9125088@localhost:9000/2", zlogsentry.WithEnvironment("dev"), zlogsentry.WithRelease("1.0.0"))
if err != nil { if err != nil {
stdlog.Fatal(err) stdlog.Fatal(err)
} }
defer w.Close() defer w.Close()
logger := zerolog.New(io.MultiWriter(w, os.Stdout)).With().Timestamp().Logger() multi := zerolog.MultiLevelWriter(os.Stdout, w)
logger := zerolog.New(multi).With().Timestamp().Logger()
logger.Error().Err(errors.New("dial timeout")).Msg("test message") logger.Error().Err(errors.New("dial timeout")).Msg("test message")
} }

7
vendor/github.com/archdx/zerolog-sentry/errors.go generated vendored Normal file
View file

@ -0,0 +1,7 @@
package zlogsentry
import (
"errors"
)
var ErrFlushTimeout = errors.New("zlogsentry flush timeout")

View file

@ -1,7 +1,10 @@
package zlogsentry package zlogsentry
import ( import (
"crypto/x509"
"errors"
"io" "io"
"net/http"
"time" "time"
"unsafe" "unsafe"
@ -27,62 +30,123 @@ var now = time.Now
type Writer struct { type Writer struct {
hub *sentry.Hub hub *sentry.Hub
levels map[zerolog.Level]struct{} levels map[zerolog.Level]struct{}
flushTimeout time.Duration flushTimeout time.Duration
withBreadcrumbs bool
} }
// Write handles zerolog's json and sends events to sentry. // addBreadcrumb adds event as a breadcrumb
func (w *Writer) Write(data []byte) (int, error) { func (w *Writer) addBreadcrumb(event *sentry.Event) {
event, ok := w.parseLogEvent(data) if !w.withBreadcrumbs {
if ok { return
w.hub.CaptureEvent(event) }
// should flush before os.Exit
if event.Level == sentry.LevelFatal { // category is totally optional, but it's nice to have
w.hub.Flush(w.flushTimeout) var category string
if _, ok := event.Extra["category"]; ok {
if v, ok := event.Extra["category"].(string); ok {
category = v
} }
} }
return len(data), nil w.hub.AddBreadcrumb(&sentry.Breadcrumb{
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) (n int, err error) {
n = len(data)
lvl, err := w.parseLogLevel(data)
if err != nil {
return n, nil
}
event, ok := w.parseLogEvent(data)
if !ok {
return
}
event.Level, ok = levelsMapping[lvl]
if !ok {
return
}
if _, enabled := w.levels[lvl]; !enabled {
// if the level is not enabled, add event as a breadcrumb
w.addBreadcrumb(event)
return
}
w.hub.CaptureEvent(event)
// should flush before os.Exit
if event.Level == sentry.LevelFatal {
w.hub.Flush(w.flushTimeout)
}
return
}
// implements zerolog.LevelWriter
func (w *Writer) WriteLevel(level zerolog.Level, p []byte) (n int, err error) {
n = len(p)
event, ok := w.parseLogEvent(p)
if !ok {
return
}
event.Level, ok = levelsMapping[level]
if !ok {
return
}
if _, enabled := w.levels[level]; !enabled {
// if the level is not enabled, add event as a breadcrumb
w.addBreadcrumb(event)
return
}
w.hub.CaptureEvent(event)
// should flush before os.Exit
if event.Level == sentry.LevelFatal {
w.hub.Flush(w.flushTimeout)
}
return
} }
// Close forces client to flush all pending events. // Close forces client to flush all pending events.
// Can be useful before application exits. // Can be useful before application exits.
func (w *Writer) Close() error { func (w *Writer) Close() error {
w.hub.Flush(w.flushTimeout) if ok := w.hub.Flush(w.flushTimeout); !ok {
return ErrFlushTimeout
}
return nil return nil
} }
// parses the log level from the encoded log
func (w *Writer) parseLogLevel(data []byte) (zerolog.Level, error) {
lvlStr, err := jsonparser.GetUnsafeString(data, zerolog.LevelFieldName)
if err != nil {
return zerolog.Disabled, nil
}
return zerolog.ParseLevel(lvlStr)
}
// parses the event except the log level
func (w *Writer) parseLogEvent(data []byte) (*sentry.Event, bool) { func (w *Writer) parseLogEvent(data []byte) (*sentry.Event, bool) {
const logger = "zerolog" const logger = "zerolog"
lvlStr, err := jsonparser.GetUnsafeString(data, zerolog.LevelFieldName)
if err != nil {
return nil, false
}
lvl, err := zerolog.ParseLevel(lvlStr)
if err != nil {
return nil, false
}
_, enabled := w.levels[lvl]
if !enabled {
return nil, false
}
sentryLvl, ok := levelsMapping[lvl]
if !ok {
return nil, false
}
event := sentry.Event{ event := sentry.Event{
Timestamp: now(), Timestamp: now(),
Level: sentryLvl,
Logger: logger, Logger: logger,
Extra: map[string]interface{}{}, Extra: map[string]interface{}{},
} }
err = jsonparser.ObjectEach(data, func(key, value []byte, vt jsonparser.ValueType, offset int) error { err := jsonparser.ObjectEach(data, func(key, value []byte, vt jsonparser.ValueType, offset int) error {
switch string(key) { switch string(key) {
case zerolog.MessageFieldName: case zerolog.MessageFieldName:
event.Message = bytesToStrUnsafe(value) event.Message = bytesToStrUnsafe(value)
@ -98,7 +162,6 @@ func (w *Writer) parseLogEvent(data []byte) (*sentry.Event, bool) {
return nil return nil
}) })
if err != nil { if err != nil {
return nil, false return nil, false
} }
@ -152,15 +215,27 @@ type optionFunc func(*config)
func (fn optionFunc) apply(c *config) { fn(c) } func (fn optionFunc) apply(c *config) { fn(c) }
type EventHintCallback func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event
type config struct { type config struct {
levels []zerolog.Level levels []zerolog.Level
sampleRate float64 sampleRate float64
release string release string
environment string environment string
serverName string serverName string
ignoreErrors []string ignoreErrors []string
debug bool breadcrumbs bool
flushTimeout time.Duration debug bool
tracing bool
debugWriter io.Writer
httpClient *http.Client
httpProxy string
httpsProxy string
caCerts *x509.CertPool
maxErrorDepth int
flushTimeout time.Duration
beforeSend sentry.EventProcessor
tracesSampleRate float64
} }
// WithLevels configures zerolog levels that have to be sent to Sentry. // WithLevels configures zerolog levels that have to be sent to Sentry.
@ -207,6 +282,13 @@ func WithIgnoreErrors(reList []string) WriterOption {
}) })
} }
// WithBreadcrumbs enables sentry client breadcrumbs.
func WithBreadcrumbs() WriterOption {
return optionFunc(func(cfg *config) {
cfg.breadcrumbs = true
})
}
// WithDebug enables sentry client debug logs. // WithDebug enables sentry client debug logs.
func WithDebug() WriterOption { func WithDebug() WriterOption {
return optionFunc(func(cfg *config) { return optionFunc(func(cfg *config) {
@ -214,6 +296,69 @@ func WithDebug() WriterOption {
}) })
} }
// WithTracing enables sentry client tracing.
func WithTracing() WriterOption {
return optionFunc(func(cfg *config) {
cfg.tracing = true
})
}
// WithTracingSampleRate sets tracing sample rate.
func WithTracingSampleRate(tsr float64) WriterOption {
return optionFunc(func(cfg *config) {
cfg.tracesSampleRate = tsr
})
}
// WithBeforeSend sets a callback which is called before event is sent.
func WithBeforeSend(beforeSend sentry.EventProcessor) WriterOption {
return optionFunc(func(cfg *config) {
cfg.beforeSend = beforeSend
})
}
// WithDebugWriter enables sentry client tracing.
func WithDebugWriter(w io.Writer) WriterOption {
return optionFunc(func(cfg *config) {
cfg.debugWriter = w
})
}
// WithHttpClient sets custom http client.
func WithHttpClient(httpClient *http.Client) WriterOption {
return optionFunc(func(cfg *config) {
cfg.httpClient = httpClient
})
}
// WithHttpProxy enables sentry client tracing.
func WithHttpProxy(proxy string) WriterOption {
return optionFunc(func(cfg *config) {
cfg.httpProxy = proxy
})
}
// WithHttpsProxy enables sentry client tracing.
func WithHttpsProxy(proxy string) WriterOption {
return optionFunc(func(cfg *config) {
cfg.httpsProxy = proxy
})
}
// WithCaCerts enables sentry client tracing.
func WithCaCerts(caCerts *x509.CertPool) WriterOption {
return optionFunc(func(cfg *config) {
cfg.caCerts = caCerts
})
}
// WithMaxErrorDepth sets the max depth of error chain.
func WithMaxErrorDepth(maxErrorDepth int) WriterOption {
return optionFunc(func(cfg *config) {
cfg.maxErrorDepth = maxErrorDepth
})
}
// New creates writer with provided DSN and options. // New creates writer with provided DSN and options.
func New(dsn string, opts ...WriterOption) (*Writer, error) { func New(dsn string, opts ...WriterOption) (*Writer, error) {
cfg := newDefaultConfig() cfg := newDefaultConfig()
@ -222,15 +367,23 @@ func New(dsn string, opts ...WriterOption) (*Writer, error) {
} }
err := sentry.Init(sentry.ClientOptions{ err := sentry.Init(sentry.ClientOptions{
Dsn: dsn, Dsn: dsn,
SampleRate: cfg.sampleRate, SampleRate: cfg.sampleRate,
Release: cfg.release, Release: cfg.release,
Environment: cfg.environment, Environment: cfg.environment,
ServerName: cfg.serverName, ServerName: cfg.serverName,
IgnoreErrors: cfg.ignoreErrors, IgnoreErrors: cfg.ignoreErrors,
Debug: cfg.debug, Debug: cfg.debug,
EnableTracing: cfg.tracing,
DebugWriter: cfg.debugWriter,
HTTPClient: cfg.httpClient,
HTTPProxy: cfg.httpProxy,
HTTPSProxy: cfg.httpsProxy,
CaCerts: cfg.caCerts,
MaxErrorDepth: cfg.maxErrorDepth,
BeforeSend: cfg.beforeSend,
TracesSampleRate: cfg.tracesSampleRate,
}) })
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -241,7 +394,31 @@ func New(dsn string, opts ...WriterOption) (*Writer, error) {
} }
return &Writer{ return &Writer{
hub: sentry.CurrentHub(), hub: sentry.CurrentHub(),
levels: levels,
flushTimeout: cfg.flushTimeout,
withBreadcrumbs: cfg.breadcrumbs,
}, nil
}
// NewWithHub creates a writer using an existing sentry Hub and options.
func NewWithHub(hub *sentry.Hub, opts ...WriterOption) (*Writer, error) {
if hub == nil {
return nil, errors.New("hub cannot be nil")
}
cfg := newDefaultConfig()
for _, opt := range opts {
opt.apply(&cfg)
}
levels := make(map[zerolog.Level]struct{}, len(cfg.levels))
for _, lvl := range cfg.levels {
levels[lvl] = struct{}{}
}
return &Writer{
hub: hub,
levels: levels, levels: levels,
flushTimeout: cfg.flushTimeout, flushTimeout: cfg.flushTimeout,
}, nil }, nil

View file

@ -1,4 +1,7 @@
language: go language: go
arch:
- amd64
- ppc64le
go: go:
- 1.7.x - 1.7.x
- 1.8.x - 1.8.x

View file

@ -1,5 +1,5 @@
[![Go Report Card](https://goreportcard.com/badge/github.com/buger/jsonparser)](https://goreportcard.com/report/github.com/buger/jsonparser) ![License](https://img.shields.io/dub/l/vibe-d.svg) [![Go Report Card](https://goreportcard.com/badge/github.com/buger/jsonparser)](https://goreportcard.com/report/github.com/buger/jsonparser) ![License](https://img.shields.io/dub/l/vibe-d.svg)
# Alternative JSON parser for Go (so far fastest) # Alternative JSON parser for Go (10x times faster standard library)
It does not require you to know the structure of the payload (eg. create structs), and allows accessing fields by providing the path to them. It is up to **10 times faster** than standard `encoding/json` package (depending on payload size and usage), **allocates no memory**. See benchmarks below. It does not require you to know the structure of the payload (eg. create structs), and allows accessing fields by providing the path to them. It is up to **10 times faster** than standard `encoding/json` package (depending on payload size and usage), **allocates no memory**. See benchmarks below.

View file

@ -6,6 +6,7 @@ import (
"reflect" "reflect"
"strconv" "strconv"
"unsafe" "unsafe"
"runtime"
) )
// //
@ -32,11 +33,12 @@ func bytesToString(b *[]byte) string {
} }
func StringToBytes(s string) []byte { func StringToBytes(s string) []byte {
b := make([]byte, 0, 0)
bh := (*reflect.SliceHeader)(unsafe.Pointer(&b))
sh := (*reflect.StringHeader)(unsafe.Pointer(&s)) sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
bh := reflect.SliceHeader{ bh.Data = sh.Data
Data: sh.Data, bh.Cap = sh.Len
Len: sh.Len, bh.Len = sh.Len
Cap: sh.Len, runtime.KeepAlive(s)
} return b
return *(*[]byte)(unsafe.Pointer(&bh))
} }

View file

@ -7,3 +7,111 @@ func FuzzParseString(data []byte) int {
} }
return 1 return 1
} }
func FuzzEachKey(data []byte) int {
paths := [][]string{
{"name"},
{"order"},
{"nested", "a"},
{"nested", "b"},
{"nested2", "a"},
{"nested", "nested3", "b"},
{"arr", "[1]", "b"},
{"arrInt", "[3]"},
{"arrInt", "[5]"},
{"nested"},
{"arr", "["},
{"a\n", "b\n"},
}
EachKey(data, func(idx int, value []byte, vt ValueType, err error) {}, paths...)
return 1
}
func FuzzDelete(data []byte) int {
Delete(data, "test")
return 1
}
func FuzzSet(data []byte) int {
_, err := Set(data, []byte(`"new value"`), "test")
if err != nil {
return 0
}
return 1
}
func FuzzObjectEach(data []byte) int {
_ = ObjectEach(data, func(key, value []byte, valueType ValueType, off int) error {
return nil
})
return 1
}
func FuzzParseFloat(data []byte) int {
_, err := ParseFloat(data)
if err != nil {
return 0
}
return 1
}
func FuzzParseInt(data []byte) int {
_, err := ParseInt(data)
if err != nil {
return 0
}
return 1
}
func FuzzParseBool(data []byte) int {
_, err := ParseBoolean(data)
if err != nil {
return 0
}
return 1
}
func FuzzTokenStart(data []byte) int {
_ = tokenStart(data)
return 1
}
func FuzzGetString(data []byte) int {
_, err := GetString(data, "test")
if err != nil {
return 0
}
return 1
}
func FuzzGetFloat(data []byte) int {
_, err := GetFloat(data, "test")
if err != nil {
return 0
}
return 1
}
func FuzzGetInt(data []byte) int {
_, err := GetInt(data, "test")
if err != nil {
return 0
}
return 1
}
func FuzzGetBoolean(data []byte) int {
_, err := GetBoolean(data, "test")
if err != nil {
return 0
}
return 1
}
func FuzzGetUnsafeString(data []byte) int {
_, err := GetUnsafeString(data, "test")
if err != nil {
return 0
}
return 1
}

47
vendor/github.com/buger/jsonparser/oss-fuzz-build.sh generated vendored Normal file
View file

@ -0,0 +1,47 @@
#!/bin/bash -eu
git clone https://github.com/dvyukov/go-fuzz-corpus
zip corpus.zip go-fuzz-corpus/json/corpus/*
cp corpus.zip $OUT/fuzzparsestring_seed_corpus.zip
compile_go_fuzzer github.com/buger/jsonparser FuzzParseString fuzzparsestring
cp corpus.zip $OUT/fuzzeachkey_seed_corpus.zip
compile_go_fuzzer github.com/buger/jsonparser FuzzEachKey fuzzeachkey
cp corpus.zip $OUT/fuzzdelete_seed_corpus.zip
compile_go_fuzzer github.com/buger/jsonparser FuzzDelete fuzzdelete
cp corpus.zip $OUT/fuzzset_seed_corpus.zip
compile_go_fuzzer github.com/buger/jsonparser FuzzSet fuzzset
cp corpus.zip $OUT/fuzzobjecteach_seed_corpus.zip
compile_go_fuzzer github.com/buger/jsonparser FuzzObjectEach fuzzobjecteach
cp corpus.zip $OUT/fuzzparsefloat_seed_corpus.zip
compile_go_fuzzer github.com/buger/jsonparser FuzzParseFloat fuzzparsefloat
cp corpus.zip $OUT/fuzzparseint_seed_corpus.zip
compile_go_fuzzer github.com/buger/jsonparser FuzzParseInt fuzzparseint
cp corpus.zip $OUT/fuzzparsebool_seed_corpus.zip
compile_go_fuzzer github.com/buger/jsonparser FuzzParseBool fuzzparsebool
cp corpus.zip $OUT/fuzztokenstart_seed_corpus.zip
compile_go_fuzzer github.com/buger/jsonparser FuzzTokenStart fuzztokenstart
cp corpus.zip $OUT/fuzzgetstring_seed_corpus.zip
compile_go_fuzzer github.com/buger/jsonparser FuzzGetString fuzzgetstring
cp corpus.zip $OUT/fuzzgetfloat_seed_corpus.zip
compile_go_fuzzer github.com/buger/jsonparser FuzzGetFloat fuzzgetfloat
cp corpus.zip $OUT/fuzzgetint_seed_corpus.zip
compile_go_fuzzer github.com/buger/jsonparser FuzzGetInt fuzzgetint
cp corpus.zip $OUT/fuzzgetboolean_seed_corpus.zip
compile_go_fuzzer github.com/buger/jsonparser FuzzGetBoolean fuzzgetboolean
cp corpus.zip $OUT/fuzzgetunsafestring_seed_corpus.zip
compile_go_fuzzer github.com/buger/jsonparser FuzzGetUnsafeString fuzzgetunsafestring

View file

@ -4,7 +4,6 @@ import (
"bytes" "bytes"
"errors" "errors"
"fmt" "fmt"
"math"
"strconv" "strconv"
) )
@ -309,7 +308,11 @@ func searchKeys(data []byte, keys ...string) int {
case '[': case '[':
// If we want to get array element by index // If we want to get array element by index
if keyLevel == level && keys[level][0] == '[' { if keyLevel == level && keys[level][0] == '[' {
aIdx, err := strconv.Atoi(keys[level][1 : len(keys[level])-1]) var keyLen = len(keys[level])
if keyLen < 3 || keys[level][0] != '[' || keys[level][keyLen-1] != ']' {
return -1
}
aIdx, err := strconv.Atoi(keys[level][1 : keyLen-1])
if err != nil { if err != nil {
return -1 return -1
} }
@ -356,14 +359,6 @@ func searchKeys(data []byte, keys ...string) int {
return -1 return -1
} }
var bitwiseFlags []int64
func init() {
for i := 0; i < 63; i++ {
bitwiseFlags = append(bitwiseFlags, int64(math.Pow(2, float64(i))))
}
}
func sameTree(p1, p2 []string) bool { func sameTree(p1, p2 []string) bool {
minLen := len(p1) minLen := len(p1)
if len(p2) < minLen { if len(p2) < minLen {
@ -380,7 +375,8 @@ func sameTree(p1, p2 []string) bool {
} }
func EachKey(data []byte, cb func(int, []byte, ValueType, error), paths ...[]string) int { func EachKey(data []byte, cb func(int, []byte, ValueType, error), paths ...[]string) int {
var pathFlags int64 var x struct{}
pathFlags := make([]bool, len(paths))
var level, pathsMatched, i int var level, pathsMatched, i int
ln := len(data) ln := len(data)
@ -422,13 +418,15 @@ func EachKey(data []byte, cb func(int, []byte, ValueType, error), paths ...[]str
// for unescape: if there are no escape sequences, this is cheap; if there are, it is a // for unescape: if there are no escape sequences, this is cheap; if there are, it is a
// bit more expensive, but causes no allocations unless len(key) > unescapeStackBufSize // bit more expensive, but causes no allocations unless len(key) > unescapeStackBufSize
var keyUnesc []byte var keyUnesc []byte
var stackbuf [unescapeStackBufSize]byte
if !keyEscaped { if !keyEscaped {
keyUnesc = key keyUnesc = key
} else if ku, err := Unescape(key, stackbuf[:]); err != nil {
return -1
} else { } else {
keyUnesc = ku var stackbuf [unescapeStackBufSize]byte
if ku, err := Unescape(key, stackbuf[:]); err != nil {
return -1
} else {
keyUnesc = ku
}
} }
if maxPath >= level { if maxPath >= level {
@ -439,17 +437,16 @@ func EachKey(data []byte, cb func(int, []byte, ValueType, error), paths ...[]str
pathsBuf[level-1] = bytesToString(&keyUnesc) pathsBuf[level-1] = bytesToString(&keyUnesc)
for pi, p := range paths { for pi, p := range paths {
if len(p) != level || pathFlags&bitwiseFlags[pi+1] != 0 || !equalStr(&keyUnesc, p[level-1]) || !sameTree(p, pathsBuf[:level]) { if len(p) != level || pathFlags[pi] || !equalStr(&keyUnesc, p[level-1]) || !sameTree(p, pathsBuf[:level]) {
continue continue
} }
match = pi match = pi
i++
pathsMatched++ pathsMatched++
pathFlags |= bitwiseFlags[pi+1] pathFlags[pi] = true
v, dt, _, e := Get(data[i:]) v, dt, _, e := Get(data[i+1:])
cb(pi, v, dt, e) cb(pi, v, dt, e)
if pathsMatched == len(paths) { if pathsMatched == len(paths) {
@ -485,8 +482,9 @@ func EachKey(data []byte, cb func(int, []byte, ValueType, error), paths ...[]str
case '}': case '}':
level-- level--
case '[': case '[':
var arrIdxFlags int64 var ok bool
var pIdxFlags int64 arrIdxFlags := make(map[int]struct{})
pIdxFlags := make([]bool, len(paths))
if level < 0 { if level < 0 {
cb(-1, nil, Unknown, MalformedJsonError) cb(-1, nil, Unknown, MalformedJsonError)
@ -494,31 +492,31 @@ func EachKey(data []byte, cb func(int, []byte, ValueType, error), paths ...[]str
} }
for pi, p := range paths { for pi, p := range paths {
if len(p) < level+1 || pathFlags&bitwiseFlags[pi+1] != 0 || p[level][0] != '[' || !sameTree(p, pathsBuf[:level]) { if len(p) < level+1 || pathFlags[pi] || p[level][0] != '[' || !sameTree(p, pathsBuf[:level]) {
continue continue
} }
if len(p[level]) >= 2 { if len(p[level]) >= 2 {
aIdx, _ := strconv.Atoi(p[level][1 : len(p[level])-1]) aIdx, _ := strconv.Atoi(p[level][1 : len(p[level])-1])
arrIdxFlags |= bitwiseFlags[aIdx+1] arrIdxFlags[aIdx] = x
pIdxFlags |= bitwiseFlags[pi+1] pIdxFlags[pi] = true
} }
} }
if arrIdxFlags > 0 { if len(arrIdxFlags) > 0 {
level++ level++
var curIdx int var curIdx int
arrOff, _ := ArrayEach(data[i:], func(value []byte, dataType ValueType, offset int, err error) { arrOff, _ := ArrayEach(data[i:], func(value []byte, dataType ValueType, offset int, err error) {
if arrIdxFlags&bitwiseFlags[curIdx+1] != 0 { if _, ok = arrIdxFlags[curIdx]; ok {
for pi, p := range paths { for pi, p := range paths {
if pIdxFlags&bitwiseFlags[pi+1] != 0 { if pIdxFlags[pi] {
aIdx, _ := strconv.Atoi(p[level-1][1 : len(p[level-1])-1]) aIdx, _ := strconv.Atoi(p[level-1][1 : len(p[level-1])-1])
if curIdx == aIdx { if curIdx == aIdx {
of := searchKeys(value, p[level:]...) of := searchKeys(value, p[level:]...)
pathsMatched++ pathsMatched++
pathFlags |= bitwiseFlags[pi+1] pathFlags[pi] = true
if of != -1 { if of != -1 {
v, dt, _, e := Get(value[of:]) v, dt, _, e := Get(value[of:])
@ -597,48 +595,96 @@ var (
) )
func createInsertComponent(keys []string, setValue []byte, comma, object bool) []byte { func createInsertComponent(keys []string, setValue []byte, comma, object bool) []byte {
var buffer bytes.Buffer
isIndex := string(keys[0][0]) == "[" isIndex := string(keys[0][0]) == "["
offset := 0
lk := calcAllocateSpace(keys, setValue, comma, object)
buffer := make([]byte, lk, lk)
if comma { if comma {
buffer.WriteString(",") offset += WriteToBuffer(buffer[offset:], ",")
} }
if isIndex && !comma { if isIndex && !comma {
buffer.WriteString("[") offset += WriteToBuffer(buffer[offset:], "[")
} else { } else {
if object { if object {
buffer.WriteString("{") offset += WriteToBuffer(buffer[offset:], "{")
} }
if !isIndex { if !isIndex {
buffer.WriteString("\"") offset += WriteToBuffer(buffer[offset:], "\"")
buffer.WriteString(keys[0]) offset += WriteToBuffer(buffer[offset:], keys[0])
buffer.WriteString("\":") offset += WriteToBuffer(buffer[offset:], "\":")
} }
} }
for i := 1; i < len(keys); i++ { for i := 1; i < len(keys); i++ {
if string(keys[i][0]) == "[" { if string(keys[i][0]) == "[" {
buffer.WriteString("[") offset += WriteToBuffer(buffer[offset:], "[")
} else { } else {
buffer.WriteString("{\"") offset += WriteToBuffer(buffer[offset:], "{\"")
buffer.WriteString(keys[i]) offset += WriteToBuffer(buffer[offset:], keys[i])
buffer.WriteString("\":") offset += WriteToBuffer(buffer[offset:], "\":")
} }
} }
buffer.Write(setValue) offset += WriteToBuffer(buffer[offset:], string(setValue))
for i := len(keys) - 1; i > 0; i-- { for i := len(keys) - 1; i > 0; i-- {
if string(keys[i][0]) == "[" { if string(keys[i][0]) == "[" {
buffer.WriteString("]") offset += WriteToBuffer(buffer[offset:], "]")
} else { } else {
buffer.WriteString("}") offset += WriteToBuffer(buffer[offset:], "}")
} }
} }
if isIndex && !comma { if isIndex && !comma {
buffer.WriteString("]") offset += WriteToBuffer(buffer[offset:], "]")
} }
if object && !isIndex { if object && !isIndex {
buffer.WriteString("}") offset += WriteToBuffer(buffer[offset:], "}")
} }
return buffer.Bytes() return buffer
}
func calcAllocateSpace(keys []string, setValue []byte, comma, object bool) int {
isIndex := string(keys[0][0]) == "["
lk := 0
if comma {
// ,
lk += 1
}
if isIndex && !comma {
// []
lk += 2
} else {
if object {
// {
lk += 1
}
if !isIndex {
// "keys[0]"
lk += len(keys[0]) + 3
}
}
lk += len(setValue)
for i := 1; i < len(keys); i++ {
if string(keys[i][0]) == "[" {
// []
lk += 2
} else {
// {"keys[i]":setValue}
lk += len(keys[i]) + 5
}
}
if object && !isIndex {
// }
lk += 1
}
return lk
}
func WriteToBuffer(buffer []byte, str string) int {
copy(buffer, str)
return len(str)
} }
/* /*
@ -766,7 +812,7 @@ func Set(data []byte, setValue []byte, keys ...string) (value []byte, err error)
if endOffset == -1 { if endOffset == -1 {
firstToken := nextToken(data) firstToken := nextToken(data)
// We can't set a top-level key if data isn't an object // We can't set a top-level key if data isn't an object
if len(data) == 0 || data[firstToken] != '{' { if firstToken < 0 || data[firstToken] != '{' {
return nil, KeyPathNotFoundError return nil, KeyPathNotFoundError
} }
// Don't need a comma if the input is an empty object // Don't need a comma if the input is an empty object
@ -916,7 +962,7 @@ func internalGet(data []byte, keys ...string) (value []byte, dataType ValueType,
value = value[1 : len(value)-1] value = value[1 : len(value)-1]
} }
return value, dataType, offset, endOffset, nil return value[:len(value):len(value)], dataType, offset, endOffset, nil
} }
// ArrayEach is used when iterating arrays, accepts a callback function with the same return arguments as `Get`. // ArrayEach is used when iterating arrays, accepts a callback function with the same return arguments as `Get`.
@ -930,7 +976,7 @@ func ArrayEach(data []byte, cb func(value []byte, dataType ValueType, offset int
return -1, MalformedJsonError return -1, MalformedJsonError
} }
offset = nT+1 offset = nT + 1
if len(keys) > 0 { if len(keys) > 0 {
if offset = searchKeys(data, keys...); offset == -1 { if offset = searchKeys(data, keys...); offset == -1 {
@ -1135,7 +1181,7 @@ func GetString(data []byte, keys ...string) (val string, err error) {
return "", fmt.Errorf("Value is not a string: %s", string(v)) return "", fmt.Errorf("Value is not a string: %s", string(v))
} }
// If no escapes return raw conten // If no escapes return raw content
if bytes.IndexByte(v, '\\') == -1 { if bytes.IndexByte(v, '\\') == -1 {
return string(v), nil return string(v), nil
} }

View file

@ -2,12 +2,9 @@ package dkim
import ( import (
"io" "io"
"regexp"
"strings" "strings"
) )
var rxReduceWS = regexp.MustCompile(`[ \t\r\n]+`)
// Canonicalization is a canonicalization algorithm. // Canonicalization is a canonicalization algorithm.
type Canonicalization string type Canonicalization string
@ -113,17 +110,15 @@ func (c *simpleCanonicalizer) CanonicalizeBody(w io.Writer) io.WriteCloser {
type relaxedCanonicalizer struct{} type relaxedCanonicalizer struct{}
func (c *relaxedCanonicalizer) CanonicalizeHeader(s string) string { func (c *relaxedCanonicalizer) CanonicalizeHeader(s string) string {
kv := strings.SplitN(s, ":", 2) k, v, ok := strings.Cut(s, ":")
if !ok {
k := strings.TrimSpace(strings.ToLower(kv[0])) return strings.TrimSpace(strings.ToLower(s)) + ":" + crlf
var v string
if len(kv) > 1 {
v = rxReduceWS.ReplaceAllString(kv[1], " ")
v = strings.TrimSpace(v)
} }
k = strings.TrimSpace(strings.ToLower(k))
v = strings.Join(strings.FieldsFunc(v, func(r rune) bool {
return r == ' ' || r == '\t' || r == '\n' || r == '\r'
}), " ")
return k + ":" + v + crlf return k + ":" + v + crlf
} }

View file

@ -1,4 +1,17 @@
// Package dkim creates and verifies DKIM signatures, as specified in RFC 6376. // Package dkim creates and verifies DKIM signatures, as specified in RFC 6376.
//
// # FAQ
//
// Why can't I verify a [net/mail.Message] directly? A [net/mail.Message]
// header is already parsed, and whitespace characters (especially continuation
// lines) are removed. Thus, the signature computed from the parsed header is
// not the same as the one computed from the raw header.
//
// How can I publish my public key? You have to add a TXT record to your DNS
// zone. See [RFC 6376 appendix C]. You can use the dkim-keygen tool included
// in go-msgauth to generate the key and the TXT record.
//
// [RFC 6376 appendix C]: https://tools.ietf.org/html/rfc6376#appendix-C
package dkim package dkim
import ( import (

View file

@ -66,28 +66,24 @@ func foldHeaderField(kv string) string {
return fold.String() + crlf return fold.String() + crlf
} }
func parseHeaderField(s string) (k string, v string) { func parseHeaderField(s string) (string, string) {
kv := strings.SplitN(s, ":", 2) key, value, _ := strings.Cut(s, ":")
k = strings.TrimSpace(kv[0]) return strings.TrimSpace(key), strings.TrimSpace(value)
if len(kv) > 1 {
v = strings.TrimSpace(kv[1])
}
return
} }
func parseHeaderParams(s string) (map[string]string, error) { func parseHeaderParams(s string) (map[string]string, error) {
pairs := strings.Split(s, ";") pairs := strings.Split(s, ";")
params := make(map[string]string) params := make(map[string]string)
for _, s := range pairs { for _, s := range pairs {
kv := strings.SplitN(s, "=", 2) key, value, ok := strings.Cut(s, "=")
if len(kv) != 2 { if !ok {
if strings.TrimSpace(s) == "" { if strings.TrimSpace(s) == "" {
continue continue
} }
return params, errors.New("dkim: malformed header params") return params, errors.New("dkim: malformed header params")
} }
params[strings.TrimSpace(kv[0])] = strings.TrimSpace(kv[1]) params[strings.TrimSpace(key)] = strings.TrimSpace(value)
} }
return params, nil return params, nil
} }
@ -149,6 +145,8 @@ func newHeaderPicker(h header) *headerPicker {
} }
func (p *headerPicker) Pick(key string) string { func (p *headerPicker) Pick(key string) string {
key = strings.ToLower(key)
at := p.picked[key] at := p.picked[key]
for i := len(p.h) - 1; i >= 0; i-- { for i := len(p.h) - 1; i >= 0; i-- {
kv := p.h[i] kv := p.h[i]

View file

@ -70,24 +70,31 @@ var queryMethods = map[QueryMethod]queryFunc{
} }
func queryDNSTXT(domain, selector string, txtLookup txtLookupFunc) (*queryResult, error) { func queryDNSTXT(domain, selector string, txtLookup txtLookupFunc) (*queryResult, error) {
var txts []string if txtLookup == nil {
var err error txtLookup = net.LookupTXT
if txtLookup != nil {
txts, err = txtLookup(selector + "._domainkey." + domain)
} else {
txts, err = net.LookupTXT(selector + "._domainkey." + domain)
} }
txts, err := txtLookup(selector + "._domainkey." + domain)
if netErr, ok := err.(net.Error); ok && netErr.Temporary() { if netErr, ok := err.(net.Error); ok && netErr.Temporary() {
return nil, tempFailError("key unavailable: " + err.Error()) return nil, tempFailError("key unavailable: " + err.Error())
} else if err != nil { } else if err != nil {
return nil, permFailError("no key for signature: " + err.Error()) return nil, permFailError("no key for signature: " + err.Error())
} }
// Long keys are split in multiple parts // net.LookupTXT will concatenate strings contained in a single TXT record.
txt := strings.Join(txts, "") // In other words, net.LookupTXT returns one entry per TXT record, even if
// a record contains multiple strings.
return parsePublicKey(txt) //
// RFC 6376 section 3.6.2.2 says multiple TXT records lead to undefined
// behavior, so reject that.
switch len(txts) {
case 0:
return nil, permFailError("no valid key found")
case 1:
return parsePublicKey(txts[0])
default:
return nil, permFailError("multiple TXT records found for key")
}
} }
func parsePublicKey(s string) (*queryResult, error) { func parsePublicKey(s string) (*queryResult, error) {

View file

@ -74,7 +74,7 @@ type SignOptions struct {
// //
// The whole message header and body must be written to the Signer. Close should // The whole message header and body must be written to the Signer. Close should
// always be called (either after the whole message has been written, or after // always be called (either after the whole message has been written, or after
// an error occured and the signer won't be used anymore). Close may return an // an error occurred and the signer won't be used anymore). Close may return an
// error in case signing fails. // error in case signing fails.
// //
// After a successful Close, Signature can be called to retrieve the // After a successful Close, Signature can be called to retrieve the

View file

@ -293,12 +293,10 @@ func verify(h header, r io.Reader, sigField, sigValue string, options *VerifyOpt
} }
// Parse algos // Parse algos
algos := strings.SplitN(stripWhitespace(params["a"]), "-", 2) keyAlgo, hashAlgo, ok := strings.Cut(stripWhitespace(params["a"]), "-")
if len(algos) != 2 { if !ok {
return verif, permFailError("malformed algorithm name") return verif, permFailError("malformed algorithm name")
} }
keyAlgo := algos[0]
hashAlgo := algos[1]
// Check hash algo // Check hash algo
if res.HashAlgos != nil { if res.HashAlgos != nil {
@ -457,6 +455,8 @@ func stripWhitespace(s string) string {
}, s) }, s)
} }
var sigRegex = regexp.MustCompile(`(b\s*=)[^;]+`)
func removeSignature(s string) string { func removeSignature(s string) string {
return regexp.MustCompile(`(b\s*=)[^;]+`).ReplaceAllString(s, "$1") return sigRegex.ReplaceAllString(s, "$1")
} }

View file

@ -1,11 +1,12 @@
# go-sasl # go-sasl
[![GoDoc](https://godoc.org/github.com/emersion/go-sasl?status.svg)](https://godoc.org/github.com/emersion/go-sasl) [![godocs.io](https://godocs.io/github.com/emersion/go-sasl?status.svg)](https://godocs.io/github.com/emersion/go-sasl)
[![Build Status](https://travis-ci.org/emersion/go-sasl.svg?branch=master)](https://travis-ci.org/emersion/go-sasl) [![Build Status](https://travis-ci.org/emersion/go-sasl.svg?branch=master)](https://travis-ci.org/emersion/go-sasl)
A [SASL](https://tools.ietf.org/html/rfc4422) library written in Go. A [SASL](https://tools.ietf.org/html/rfc4422) library written in Go.
Implemented mechanisms: Implemented mechanisms:
* [ANONYMOUS](https://tools.ietf.org/html/rfc4505) * [ANONYMOUS](https://tools.ietf.org/html/rfc4505)
* [EXTERNAL](https://tools.ietf.org/html/rfc4422#appendix-A) * [EXTERNAL](https://tools.ietf.org/html/rfc4422#appendix-A)
* [LOGIN](https://tools.ietf.org/html/draft-murchison-sasl-login-00) (obsolete, use PLAIN instead) * [LOGIN](https://tools.ietf.org/html/draft-murchison-sasl-login-00) (obsolete, use PLAIN instead)

View file

@ -27,7 +27,7 @@ func NewAnonymousClient(trace string) Client {
type AnonymousAuthenticator func(trace string) error type AnonymousAuthenticator func(trace string) error
type anonymousServer struct { type anonymousServer struct {
done bool done bool
authenticate AnonymousAuthenticator authenticate AnonymousAuthenticator
} }

View file

@ -1,5 +1,10 @@
package sasl package sasl
import (
"bytes"
"errors"
)
// The EXTERNAL mechanism name. // The EXTERNAL mechanism name.
const External = "EXTERNAL" const External = "EXTERNAL"
@ -24,3 +29,39 @@ func (a *externalClient) Next(challenge []byte) (response []byte, err error) {
func NewExternalClient(identity string) Client { func NewExternalClient(identity string) Client {
return &externalClient{identity} return &externalClient{identity}
} }
// ExternalAuthenticator authenticates users with the EXTERNAL mechanism. If
// the identity is left blank, it indicates that it is the same as the one used
// in the external credentials. If identity is not empty and the server doesn't
// support it, an error must be returned.
type ExternalAuthenticator func(identity string) error
type externalServer struct {
done bool
authenticate ExternalAuthenticator
}
func (a *externalServer) Next(response []byte) (challenge []byte, done bool, err error) {
if a.done {
return nil, false, ErrUnexpectedClientResponse
}
// No initial response, send an empty challenge
if response == nil {
return []byte{}, false, nil
}
a.done = true
if bytes.Contains(response, []byte("\x00")) {
return nil, false, errors.New("sasl: identity contains a NUL character")
}
return nil, true, a.authenticate(string(response))
}
// NewExternalServer creates a server implementation of the EXTERNAL
// authentication mechanism, as described in RFC 4422.
func NewExternalServer(authenticator ExternalAuthenticator) Server {
return &externalServer{authenticate: authenticator}
}

View file

@ -35,8 +35,11 @@ type oauthBearerClient struct {
} }
func (a *oauthBearerClient) Start() (mech string, ir []byte, err error) { func (a *oauthBearerClient) Start() (mech string, ir []byte, err error) {
mech = OAuthBearer var authzid string
var str = "n,a=" + a.Username + "," if a.Username != "" {
authzid = "a=" + a.Username
}
str := "n," + authzid + ","
if a.Host != "" { if a.Host != "" {
str += "\x01host=" + a.Host str += "\x01host=" + a.Host
@ -47,7 +50,7 @@ func (a *oauthBearerClient) Start() (mech string, ir []byte, err error) {
} }
str += "\x01auth=Bearer " + a.Token + "\x01\x01" str += "\x01auth=Bearer " + a.Token + "\x01\x01"
ir = []byte(str) ir = []byte(str)
return return OAuthBearer, ir, nil
} }
func (a *oauthBearerClient) Next(challenge []byte) ([]byte, error) { func (a *oauthBearerClient) Next(challenge []byte) ([]byte, error) {
@ -81,7 +84,7 @@ func (a *oauthBearerServer) fail(descr string) ([]byte, bool, error) {
if err != nil { if err != nil {
panic(err) // wtf panic(err) // wtf
} }
a.failErr = errors.New(descr) a.failErr = errors.New("sasl: client error: " + descr)
return blob, false, nil return blob, false, nil
} }
@ -95,7 +98,7 @@ func (a *oauthBearerServer) Next(response []byte) (challenge []byte, done bool,
// indirectly OAUTHBEARER) defines a protocol-independent way to do so // indirectly OAUTHBEARER) defines a protocol-independent way to do so
// using 0x01. // using 0x01.
if len(response) != 1 && response[0] != 0x01 { if len(response) != 1 && response[0] != 0x01 {
return nil, true, errors.New("unexpected response") return nil, true, errors.New("sasl: invalid response")
} }
return nil, true, a.failErr return nil, true, a.failErr
} }
@ -121,14 +124,18 @@ func (a *oauthBearerServer) Next(response []byte) (challenge []byte, done bool,
if len(parts) != 3 { if len(parts) != 3 {
return a.fail("Invalid response") return a.fail("Invalid response")
} }
if !bytes.Equal(parts[0], []byte{'n'}) { flag := parts[0]
return a.fail("Invalid response, missing 'n'") authzid := parts[1]
if !bytes.Equal(flag, []byte{'n'}) {
return a.fail("Invalid response, missing 'n' in gs2-cb-flag")
} }
opts := OAuthBearerOptions{} opts := OAuthBearerOptions{}
if !bytes.HasPrefix(parts[1], []byte("a=")) { if len(authzid) > 0 {
return a.fail("Invalid response, missing 'a'") if !bytes.HasPrefix(authzid, []byte("a=")) {
return a.fail("Invalid response, missing 'a=' in gs2-authzid")
}
opts.Username = string(bytes.TrimPrefix(authzid, []byte("a=")))
} }
opts.Username = string(bytes.TrimPrefix(parts[1], []byte("a=")))
// Cut \x01host=...\x01auth=...\x01\x01 // Cut \x01host=...\x01auth=...\x01\x01
// into // into

View file

@ -38,7 +38,7 @@ func NewPlainClient(identity, username, password string) Client {
type PlainAuthenticator func(identity, username, password string) error type PlainAuthenticator func(identity, username, password string) error
type plainServer struct { type plainServer struct {
done bool done bool
authenticate PlainAuthenticator authenticate PlainAuthenticator
} }
@ -57,7 +57,7 @@ func (a *plainServer) Next(response []byte) (challenge []byte, done bool, err er
parts := bytes.Split(response, []byte("\x00")) parts := bytes.Split(response, []byte("\x00"))
if len(parts) != 3 { if len(parts) != 3 {
err = errors.New("Invalid response") err = errors.New("sasl: invalid response")
return return
} }

View file

@ -12,7 +12,7 @@ import (
// Common SASL errors. // Common SASL errors.
var ( var (
ErrUnexpectedClientResponse = errors.New("sasl: unexpected client response") ErrUnexpectedClientResponse = errors.New("sasl: unexpected client response")
ErrUnexpectedServerChallenge = errors.New("sasl: unexpected server challenge") ErrUnexpectedServerChallenge = errors.New("sasl: unexpected server challenge")
) )

View file

@ -1,11 +1,10 @@
image: alpine/edge image: alpine/edge
packages: packages:
- go - go
# Required by codecov
- bash
- findutils
sources: sources:
- https://github.com/emersion/go-smtp - https://github.com/emersion/go-smtp
artifacts:
- coverage.html
tasks: tasks:
- build: | - build: |
cd go-smtp cd go-smtp
@ -13,7 +12,6 @@ tasks:
- test: | - test: |
cd go-smtp cd go-smtp
go test -coverprofile=coverage.txt -covermode=atomic ./... go test -coverprofile=coverage.txt -covermode=atomic ./...
- upload-coverage: | - coverage: |
cd go-smtp cd go-smtp
export CODECOV_TOKEN=3f257f71-a128-4834-8f68-2b534e9f4cb1 go tool cover -html=coverage.txt -o ~/coverage.html
curl -s https://codecov.io/bash | bash

View file

@ -1,144 +1,16 @@
# go-smtp # go-smtp
[![godocs.io](https://godocs.io/github.com/emersion/go-smtp?status.svg)](https://godocs.io/github.com/emersion/go-smtp) [![Go Reference](https://pkg.go.dev/badge/github.com/emersion/go-smtp.svg)](https://pkg.go.dev/github.com/emersion/go-smtp)
[![builds.sr.ht status](https://builds.sr.ht/~emersion/go-smtp/commits.svg)](https://builds.sr.ht/~emersion/go-smtp/commits?) [![builds.sr.ht status](https://builds.sr.ht/~emersion/go-smtp/commits.svg)](https://builds.sr.ht/~emersion/go-smtp/commits?)
[![codecov](https://codecov.io/gh/emersion/go-smtp/branch/master/graph/badge.svg)](https://codecov.io/gh/emersion/go-smtp)
An ESMTP client and server library written in Go. An ESMTP client and server library written in Go.
## Features ## Features
* ESMTP client & server implementing [RFC 5321](https://tools.ietf.org/html/rfc5321) * ESMTP client & server implementing [RFC 5321]
* Support for SMTP [AUTH](https://tools.ietf.org/html/rfc4954) and [PIPELINING](https://tools.ietf.org/html/rfc2920) * Support for additional SMTP extensions such as [AUTH] and [PIPELINING]
* UTF-8 support for subject and message * UTF-8 support for subject and message
* [LMTP](https://tools.ietf.org/html/rfc2033) support * [LMTP] support
## Usage
### Client
```go
package main
import (
"log"
"strings"
"github.com/emersion/go-sasl"
"github.com/emersion/go-smtp"
)
func main() {
// Set up authentication information.
auth := sasl.NewPlainClient("", "user@example.com", "password")
// Connect to the server, authenticate, set the sender and recipient,
// and send the email all in one step.
to := []string{"recipient@example.net"}
msg := strings.NewReader("To: recipient@example.net\r\n" +
"Subject: discount Gophers!\r\n" +
"\r\n" +
"This is the email body.\r\n")
err := smtp.SendMail("mail.example.com:25", auth, "sender@example.org", to, msg)
if err != nil {
log.Fatal(err)
}
}
```
If you need more control, you can use `Client` instead.
### Server
```go
package main
import (
"errors"
"io"
"io/ioutil"
"log"
"time"
"github.com/emersion/go-smtp"
)
// The Backend implements SMTP server methods.
type Backend struct{}
// Login handles a login command with username and password.
func (bkd *Backend) Login(state *smtp.ConnectionState, username, password string) (smtp.Session, error) {
if username != "username" || password != "password" {
return nil, errors.New("Invalid username or password")
}
return &Session{}, nil
}
// AnonymousLogin requires clients to authenticate using SMTP AUTH before sending emails
func (bkd *Backend) AnonymousLogin(state *smtp.ConnectionState) (smtp.Session, error) {
return nil, smtp.ErrAuthRequired
}
// A Session is returned after successful login.
type Session struct{}
func (s *Session) Mail(from string, opts smtp.MailOptions) error {
log.Println("Mail from:", from)
return nil
}
func (s *Session) Rcpt(to string) error {
log.Println("Rcpt to:", to)
return nil
}
func (s *Session) Data(r io.Reader) error {
if b, err := ioutil.ReadAll(r); err != nil {
return err
} else {
log.Println("Data:", string(b))
}
return nil
}
func (s *Session) Reset() {}
func (s *Session) Logout() error {
return nil
}
func main() {
be := &Backend{}
s := smtp.NewServer(be)
s.Addr = ":1025"
s.Domain = "localhost"
s.ReadTimeout = 10 * time.Second
s.WriteTimeout = 10 * time.Second
s.MaxMessageBytes = 1024 * 1024
s.MaxRecipients = 50
s.AllowInsecureAuth = true
log.Println("Starting server at", s.Addr)
if err := s.ListenAndServe(); err != nil {
log.Fatal(err)
}
}
```
You can use the server manually with `telnet`:
```
$ telnet localhost 1025
EHLO localhost
AUTH PLAIN
AHVzZXJuYW1lAHBhc3N3b3Jk
MAIL FROM:<root@nsa.gov>
RCPT TO:<root@gchq.gov.uk>
DATA
Hey <3
.
```
## Relationship with net/smtp ## Relationship with net/smtp
@ -149,3 +21,8 @@ provides a server implementation and a number of client improvements.
## Licence ## Licence
MIT MIT
[RFC 5321]: https://tools.ietf.org/html/rfc5321
[AUTH]: https://tools.ietf.org/html/rfc4954
[PIPELINING]: https://tools.ietf.org/html/rfc2920
[LMTP]: https://tools.ietf.org/html/rfc2033

View file

@ -1,61 +1,30 @@
package smtp package smtp
import ( import (
"errors"
"io" "io"
) )
var ( var (
ErrAuthRequired = errors.New("Please authenticate first") ErrAuthFailed = &SMTPError{
ErrAuthUnsupported = errors.New("Authentication not supported") Code: 535,
EnhancedCode: EnhancedCode{5, 7, 8},
Message: "Authentication failed",
}
ErrAuthRequired = &SMTPError{
Code: 502,
EnhancedCode: EnhancedCode{5, 7, 0},
Message: "Please authenticate first",
}
ErrAuthUnsupported = &SMTPError{
Code: 502,
EnhancedCode: EnhancedCode{5, 7, 0},
Message: "Authentication not supported",
}
) )
// A SMTP server backend. // A SMTP server backend.
type Backend interface { type Backend interface {
// Authenticate a user. Return smtp.ErrAuthUnsupported if you don't want to NewSession(c *Conn) (Session, error)
// support this.
Login(state *ConnectionState, username, password string) (Session, error)
// Called if the client attempts to send mail without logging in first.
// Return smtp.ErrAuthRequired if you don't want to support this.
AnonymousLogin(state *ConnectionState) (Session, error)
}
type BodyType string
const (
Body7Bit BodyType = "7BIT"
Body8BitMIME BodyType = "8BITMIME"
BodyBinaryMIME BodyType = "BINARYMIME"
)
// MailOptions contains custom arguments that were
// passed as an argument to the MAIL command.
type MailOptions struct {
// Value of BODY= argument, 7BIT, 8BITMIME or BINARYMIME.
Body BodyType
// Size of the body. Can be 0 if not specified by client.
Size int
// TLS is required for the message transmission.
//
// The message should be rejected if it can't be transmitted
// with TLS.
RequireTLS bool
// The message envelope or message header contains UTF-8-encoded strings.
// This flag is set by SMTPUTF8-aware (RFC 6531) client.
UTF8 bool
// The authorization identity asserted by the message sender in decoded
// form with angle brackets stripped.
//
// nil value indicates missing AUTH, non-nil empty string indicates
// AUTH=<>.
//
// Defined in RFC 4954.
Auth *string
} }
// Session is used by servers to respond to an SMTP client. // Session is used by servers to respond to an SMTP client.
@ -68,17 +37,24 @@ type Session interface {
// Free all resources associated with session. // Free all resources associated with session.
Logout() error Logout() error
// Authenticate the user using SASL PLAIN.
AuthPlain(username, password string) error
// Set return path for currently processed message. // Set return path for currently processed message.
Mail(from string, opts MailOptions) error Mail(from string, opts *MailOptions) error
// Add recipient for currently processed message. // Add recipient for currently processed message.
Rcpt(to string) error Rcpt(to string, opts *RcptOptions) error
// Set currently processed message contents and send it. // Set currently processed message contents and send it.
//
// r must be consumed before Data returns.
Data(r io.Reader) error Data(r io.Reader) error
} }
// LMTPSession is an add-on interface for Session. It can be implemented by // LMTPSession is an add-on interface for Session. It can be implemented by
// LMTP servers to provide extra functionality. // LMTP servers to provide extra functionality.
type LMTPSession interface { type LMTPSession interface {
Session
// LMTPData is the LMTP-specific version of Data method. // LMTPData is the LMTP-specific version of Data method.
// It can be optionally implemented by the backend to provide // It can be optionally implemented by the backend to provide
// per-recipient status information when it is used over LMTP // per-recipient status information when it is used over LMTP

View file

@ -21,15 +21,10 @@ import (
// A Client represents a client connection to an SMTP server. // A Client represents a client connection to an SMTP server.
type Client struct { type Client struct {
// Text is the textproto.Conn used by the Client. It is exported to allow for
// clients to add extensions.
Text *textproto.Conn
// keep a reference to the connection so it can be used to create a TLS // keep a reference to the connection so it can be used to create a TLS
// connection later // connection later
conn net.Conn conn net.Conn
// whether the Client is using TLS text *textproto.Conn
tls bool
serverName string serverName string
lmtp bool lmtp bool
// map of supported extensions // map of supported extensions
@ -50,15 +45,24 @@ type Client struct {
DebugWriter io.Writer DebugWriter io.Writer
} }
// Dial returns a new Client connected to an SMTP server at addr. // 30 seconds was chosen as it's the same duration as http.DefaultTransport's
// The addr must include a port, as in "mail.example.com:smtp". // timeout.
const defaultTimeout = 30 * time.Second
var defaultDialer = net.Dialer{Timeout: defaultTimeout}
// Dial returns a new Client connected to an SMTP server at addr. The addr must
// include a port, as in "mail.example.com:smtp".
//
// This function returns a plaintext connection. To enable TLS, use StartTLS.
func Dial(addr string) (*Client, error) { func Dial(addr string) (*Client, error) {
conn, err := net.Dial("tcp", addr) conn, err := defaultDialer.Dial("tcp", addr)
if err != nil { if err != nil {
return nil, err return nil, err
} }
host, _, _ := net.SplitHostPort(addr) client := NewClient(conn)
return NewClient(conn, host) client.serverName, _, _ = net.SplitHostPort(addr)
return client, nil
} }
// DialTLS returns a new Client connected to an SMTP server via TLS at addr. // DialTLS returns a new Client connected to an SMTP server via TLS at addr.
@ -66,20 +70,24 @@ func Dial(addr string) (*Client, error) {
// //
// A nil tlsConfig is equivalent to a zero tls.Config. // A nil tlsConfig is equivalent to a zero tls.Config.
func DialTLS(addr string, tlsConfig *tls.Config) (*Client, error) { func DialTLS(addr string, tlsConfig *tls.Config) (*Client, error) {
conn, err := tls.Dial("tcp", addr, tlsConfig) tlsDialer := tls.Dialer{
NetDialer: &defaultDialer,
Config: tlsConfig,
}
conn, err := tlsDialer.Dial("tcp", addr)
if err != nil { if err != nil {
return nil, err return nil, err
} }
host, _, _ := net.SplitHostPort(addr) client := NewClient(conn)
return NewClient(conn, host) client.serverName, _, _ = net.SplitHostPort(addr)
return client, nil
} }
// NewClient returns a new Client using an existing connection and host as a // NewClient returns a new Client using an existing connection and host as a
// server name to be used when authenticating. // server name to be used when authenticating.
func NewClient(conn net.Conn, host string) (*Client, error) { func NewClient(conn net.Conn) *Client {
c := &Client{ c := &Client{
serverName: host, localName: "localhost",
localName: "localhost",
// As recommended by RFC 5321. For DATA command reply (3xx one) RFC // As recommended by RFC 5321. For DATA command reply (3xx one) RFC
// recommends a slightly shorter timeout but we do not bother // recommends a slightly shorter timeout but we do not bother
// differentiating these. // differentiating these.
@ -91,27 +99,15 @@ func NewClient(conn net.Conn, host string) (*Client, error) {
c.setConn(conn) c.setConn(conn)
_, _, err := c.Text.ReadResponse(220) return c
if err != nil {
c.Text.Close()
if protoErr, ok := err.(*textproto.Error); ok {
return nil, toSMTPErr(protoErr)
}
return nil, err
}
return c, nil
} }
// NewClientLMTP returns a new LMTP Client (as defined in RFC 2033) using an // NewClientLMTP returns a new LMTP Client (as defined in RFC 2033) using an
// existing connector and host as a server name to be used when authenticating. // existing connection and host as a server name to be used when authenticating.
func NewClientLMTP(conn net.Conn, host string) (*Client, error) { func NewClientLMTP(conn net.Conn) *Client {
c, err := NewClient(conn, host) c := NewClient(conn)
if err != nil {
return nil, err
}
c.lmtp = true c.lmtp = true
return c, nil return c
} }
// setConn sets the underlying network connection for the client. // setConn sets the underlying network connection for the client.
@ -139,23 +135,38 @@ func (c *Client) setConn(conn net.Conn) {
Writer: w, Writer: w,
Closer: conn, Closer: conn,
} }
c.Text = textproto.NewConn(rwc) c.text = textproto.NewConn(rwc)
_, isTLS := conn.(*tls.Conn)
c.tls = isTLS
} }
// Close closes the connection. // Close closes the connection.
func (c *Client) Close() error { func (c *Client) Close() error {
return c.Text.Close() return c.text.Close()
}
func (c *Client) greet() error {
// Initial greeting timeout. RFC 5321 recommends 5 minutes.
c.conn.SetDeadline(time.Now().Add(c.CommandTimeout))
defer c.conn.SetDeadline(time.Time{})
_, _, err := c.text.ReadResponse(220)
if err != nil {
c.text.Close()
if protoErr, ok := err.(*textproto.Error); ok {
return toSMTPErr(protoErr)
}
return err
}
return nil
} }
// hello runs a hello exchange if needed. // hello runs a hello exchange if needed.
func (c *Client) hello() error { func (c *Client) hello() error {
if !c.didHello { if !c.didHello {
c.didHello = true c.didHello = true
err := c.ehlo() if err := c.greet(); err != nil {
if err != nil { c.helloError = err
} else if err := c.ehlo(); err != nil {
c.helloError = c.helo() c.helloError = c.helo()
} }
} }
@ -181,18 +192,18 @@ func (c *Client) Hello(localName string) error {
} }
// cmd is a convenience function that sends a command and returns the response // cmd is a convenience function that sends a command and returns the response
// textproto.Error returned by c.Text.ReadResponse is converted into SMTPError. // textproto.Error returned by c.text.ReadResponse is converted into SMTPError.
func (c *Client) cmd(expectCode int, format string, args ...interface{}) (int, string, error) { func (c *Client) cmd(expectCode int, format string, args ...interface{}) (int, string, error) {
c.conn.SetDeadline(time.Now().Add(c.CommandTimeout)) c.conn.SetDeadline(time.Now().Add(c.CommandTimeout))
defer c.conn.SetDeadline(time.Time{}) defer c.conn.SetDeadline(time.Time{})
id, err := c.Text.Cmd(format, args...) id, err := c.text.Cmd(format, args...)
if err != nil { if err != nil {
return 0, "", err return 0, "", err
} }
c.Text.StartResponse(id) c.text.StartResponse(id)
defer c.Text.EndResponse(id) defer c.text.EndResponse(id)
code, msg, err := c.Text.ReadResponse(expectCode) code, msg, err := c.text.ReadResponse(expectCode)
if err != nil { if err != nil {
if protoErr, ok := err.(*textproto.Error); ok { if protoErr, ok := err.(*textproto.Error); ok {
smtpErr := toSMTPErr(protoErr) smtpErr := toSMTPErr(protoErr)
@ -260,7 +271,7 @@ func (c *Client) StartTLS(config *tls.Config) error {
if config == nil { if config == nil {
config = &tls.Config{} config = &tls.Config{}
} }
if config.ServerName == "" { if config.ServerName == "" && c.serverName != "" {
// Make a copy to avoid polluting argument // Make a copy to avoid polluting argument
config = config.Clone() config = config.Clone()
config.ServerName = c.serverName config.ServerName = c.serverName
@ -365,34 +376,54 @@ func (c *Client) Mail(from string, opts *MailOptions) error {
if err := c.hello(); err != nil { if err := c.hello(); err != nil {
return err return err
} }
cmdStr := "MAIL FROM:<%s>"
var sb strings.Builder
// A high enough power of 2 than 510+14+26+11+9+9+39+500
sb.Grow(2048)
fmt.Fprintf(&sb, "MAIL FROM:<%s>", from)
if _, ok := c.ext["8BITMIME"]; ok { if _, ok := c.ext["8BITMIME"]; ok {
cmdStr += " BODY=8BITMIME" sb.WriteString(" BODY=8BITMIME")
} }
if _, ok := c.ext["SIZE"]; ok && opts != nil && opts.Size != 0 { if _, ok := c.ext["SIZE"]; ok && opts != nil && opts.Size != 0 {
cmdStr += " SIZE=" + strconv.Itoa(opts.Size) fmt.Fprintf(&sb, " SIZE=%v", opts.Size)
} }
if opts != nil && opts.RequireTLS { if opts != nil && opts.RequireTLS {
if _, ok := c.ext["REQUIRETLS"]; ok { if _, ok := c.ext["REQUIRETLS"]; ok {
cmdStr += " REQUIRETLS" sb.WriteString(" REQUIRETLS")
} else { } else {
return errors.New("smtp: server does not support REQUIRETLS") return errors.New("smtp: server does not support REQUIRETLS")
} }
} }
if opts != nil && opts.UTF8 { if opts != nil && opts.UTF8 {
if _, ok := c.ext["SMTPUTF8"]; ok { if _, ok := c.ext["SMTPUTF8"]; ok {
cmdStr += " SMTPUTF8" sb.WriteString(" SMTPUTF8")
} else { } else {
return errors.New("smtp: server does not support SMTPUTF8") return errors.New("smtp: server does not support SMTPUTF8")
} }
} }
if _, ok := c.ext["DSN"]; ok && opts != nil {
switch opts.Return {
case DSNReturnFull, DSNReturnHeaders:
fmt.Fprintf(&sb, " RET=%s", string(opts.Return))
case "":
// This space is intentionally left blank
default:
return errors.New("smtp: Unknown RET parameter value")
}
if opts.EnvelopeID != "" {
if !isPrintableASCII(opts.EnvelopeID) {
return errors.New("smtp: Malformed ENVID parameter value")
}
fmt.Fprintf(&sb, " ENVID=%s", encodeXtext(opts.EnvelopeID))
}
}
if opts != nil && opts.Auth != nil { if opts != nil && opts.Auth != nil {
if _, ok := c.ext["AUTH"]; ok { if _, ok := c.ext["AUTH"]; ok {
cmdStr += " AUTH=" + encodeXtext(*opts.Auth) fmt.Fprintf(&sb, " AUTH=%s", encodeXtext(*opts.Auth))
} }
// We can safely discard parameter if server does not support AUTH. // We can safely discard parameter if server does not support AUTH.
} }
_, _, err := c.cmd(250, cmdStr, from) _, _, err := c.cmd(250, "%s", sb.String())
return err return err
} }
@ -400,12 +431,53 @@ func (c *Client) Mail(from string, opts *MailOptions) error {
// A call to Rcpt must be preceded by a call to Mail and may be followed by // A call to Rcpt must be preceded by a call to Mail and may be followed by
// a Data call or another Rcpt call. // a Data call or another Rcpt call.
// //
// If opts is not nil, RCPT arguments provided in the structure will be added
// to the command. Handling of unsupported options depends on the extension.
//
// If server returns an error, it will be of type *SMTPError. // If server returns an error, it will be of type *SMTPError.
func (c *Client) Rcpt(to string) error { func (c *Client) Rcpt(to string, opts *RcptOptions) error {
if err := validateLine(to); err != nil { if err := validateLine(to); err != nil {
return err return err
} }
if _, _, err := c.cmd(25, "RCPT TO:<%s>", to); err != nil {
var sb strings.Builder
// A high enough power of 2 than 510+29+501
sb.Grow(2048)
fmt.Fprintf(&sb, "RCPT TO:<%s>", to)
if _, ok := c.ext["DSN"]; ok && opts != nil {
if opts.Notify != nil && len(opts.Notify) != 0 {
sb.WriteString(" NOTIFY=")
if err := checkNotifySet(opts.Notify); err != nil {
return errors.New("smtp: Malformed NOTIFY parameter value")
}
for i, v := range opts.Notify {
if i != 0 {
sb.WriteString(",")
}
sb.WriteString(string(v))
}
}
if opts.OriginalRecipient != "" {
var enc string
switch opts.OriginalRecipientType {
case DSNAddressTypeRFC822:
if !isPrintableASCII(opts.OriginalRecipient) {
return errors.New("smtp: Illegal address")
}
enc = encodeXtext(opts.OriginalRecipient)
case DSNAddressTypeUTF8:
if _, ok := c.ext["SMTPUTF8"]; ok {
enc = encodeUTF8AddrUnitext(opts.OriginalRecipient)
} else {
enc = encodeUTF8AddrXtext(opts.OriginalRecipient)
}
default:
return errors.New("smtp: Unknown address type")
}
fmt.Fprintf(&sb, " ORCPT=%s;%s", string(opts.OriginalRecipientType), enc)
}
}
if _, _, err := c.cmd(25, "%s", sb.String()); err != nil {
return err return err
} }
c.rcpts = append(c.rcpts, to) c.rcpts = append(c.rcpts, to)
@ -416,10 +488,17 @@ type dataCloser struct {
c *Client c *Client
io.WriteCloser io.WriteCloser
statusCb func(rcpt string, status *SMTPError) statusCb func(rcpt string, status *SMTPError)
closed bool
} }
func (d *dataCloser) Close() error { func (d *dataCloser) Close() error {
d.WriteCloser.Close() if d.closed {
return fmt.Errorf("smtp: data writer closed twice")
}
if err := d.WriteCloser.Close(); err != nil {
return err
}
d.c.conn.SetDeadline(time.Now().Add(d.c.SubmissionTimeout)) d.c.conn.SetDeadline(time.Now().Add(d.c.SubmissionTimeout))
defer d.c.conn.SetDeadline(time.Time{}) defer d.c.conn.SetDeadline(time.Time{})
@ -428,7 +507,7 @@ func (d *dataCloser) Close() error {
if d.c.lmtp { if d.c.lmtp {
for expectedResponses > 0 { for expectedResponses > 0 {
rcpt := d.c.rcpts[len(d.c.rcpts)-expectedResponses] rcpt := d.c.rcpts[len(d.c.rcpts)-expectedResponses]
if _, _, err := d.c.Text.ReadResponse(250); err != nil { if _, _, err := d.c.text.ReadResponse(250); err != nil {
if protoErr, ok := err.(*textproto.Error); ok { if protoErr, ok := err.(*textproto.Error); ok {
if d.statusCb != nil { if d.statusCb != nil {
d.statusCb(rcpt, toSMTPErr(protoErr)) d.statusCb(rcpt, toSMTPErr(protoErr))
@ -441,17 +520,18 @@ func (d *dataCloser) Close() error {
} }
expectedResponses-- expectedResponses--
} }
return nil
} else { } else {
_, _, err := d.c.Text.ReadResponse(250) _, _, err := d.c.text.ReadResponse(250)
if err != nil { if err != nil {
if protoErr, ok := err.(*textproto.Error); ok { if protoErr, ok := err.(*textproto.Error); ok {
return toSMTPErr(protoErr) return toSMTPErr(protoErr)
} }
return err return err
} }
return nil
} }
d.closed = true
return nil
} }
// Data issues a DATA command to the server and returns a writer that // Data issues a DATA command to the server and returns a writer that
@ -465,7 +545,7 @@ func (c *Client) Data() (io.WriteCloser, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &dataCloser{c, c.Text.DotWriter(), nil}, nil return &dataCloser{c: c, WriteCloser: c.text.DotWriter()}, nil
} }
// LMTPData is the LMTP-specific version of the Data method. It accepts a callback // LMTPData is the LMTP-specific version of the Data method. It accepts a callback
@ -485,16 +565,51 @@ func (c *Client) LMTPData(statusCb func(rcpt string, status *SMTPError)) (io.Wri
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &dataCloser{c, c.Text.DotWriter(), statusCb}, nil return &dataCloser{c: c, WriteCloser: c.text.DotWriter(), statusCb: statusCb}, nil
}
// SendMail will use an existing connection to send an email from
// address from, to addresses to, with message r.
//
// This function does not start TLS, nor does it perform authentication. Use
// StartTLS and Auth before-hand if desirable.
//
// The addresses in the to parameter are the SMTP RCPT addresses.
//
// The r parameter should be an RFC 822-style email with headers
// first, a blank line, and then the message body. The lines of r
// should be CRLF terminated. The r headers should usually include
// fields such as "From", "To", "Subject", and "Cc". Sending "Bcc"
// messages is accomplished by including an email address in the to
// parameter but not including it in the r headers.
func (c *Client) SendMail(from string, to []string, r io.Reader) error {
var err error
if err = c.Mail(from, nil); err != nil {
return err
}
for _, addr := range to {
if err = c.Rcpt(addr, nil); err != nil {
return err
}
}
w, err := c.Data()
if err != nil {
return err
}
_, err = io.Copy(w, r)
if err != nil {
return err
}
return w.Close()
} }
var testHookStartTLS func(*tls.Config) // nil, except for tests var testHookStartTLS func(*tls.Config) // nil, except for tests
// SendMail connects to the server at addr, switches to TLS if // SendMail connects to the server at addr, switches to TLS, authenticates with
// possible, authenticates with the optional mechanism a if possible, // the optional SASL client, and then sends an email from address from, to
// and then sends an email from address from, to addresses to, with // addresses to, with message r. The addr must include a port, as in
// message r. // "mail.example.com:smtp".
// The addr must include a port, as in "mail.example.com:smtp".
// //
// The addresses in the to parameter are the SMTP RCPT addresses. // The addresses in the to parameter are the SMTP RCPT addresses.
// //
@ -521,45 +636,65 @@ func SendMail(addr string, a sasl.Client, from string, to []string, r io.Reader)
return err return err
} }
} }
c, err := Dial(addr) c, err := Dial(addr)
if err != nil { if err != nil {
return err return err
} }
defer c.Close() defer c.Close()
if err = c.hello(); err != nil { if err = c.hello(); err != nil {
return err return err
} }
if ok, _ := c.Extension("STARTTLS"); ok { if ok, _ := c.Extension("STARTTLS"); !ok {
if err = c.StartTLS(nil); err != nil { return errors.New("smtp: server doesn't support STARTTLS")
return err
}
} }
if a != nil && c.ext != nil { if err = c.StartTLS(nil); err != nil {
if _, ok := c.ext["AUTH"]; !ok { return err
}
if a != nil {
if ok, _ := c.Extension("AUTH"); !ok {
return errors.New("smtp: server doesn't support AUTH") return errors.New("smtp: server doesn't support AUTH")
} }
if err = c.Auth(a); err != nil { if err = c.Auth(a); err != nil {
return err return err
} }
} }
if err = c.Mail(from, nil); err != nil { if err := c.SendMail(from, to, r); err != nil {
return err return err
} }
for _, addr := range to { return c.Quit()
if err = c.Rcpt(addr); err != nil { }
// SendMailTLS works like SendMail, but with implicit TLS.
func SendMailTLS(addr string, a sasl.Client, from string, to []string, r io.Reader) error {
if err := validateLine(from); err != nil {
return err
}
for _, recp := range to {
if err := validateLine(recp); err != nil {
return err return err
} }
} }
w, err := c.Data()
c, err := DialTLS(addr, nil)
if err != nil { if err != nil {
return err return err
} }
_, err = io.Copy(w, r) defer c.Close()
if err != nil {
if err = c.hello(); err != nil {
return err return err
} }
err = w.Close() if a != nil {
if err != nil { if ok, _ := c.Extension("AUTH"); !ok {
return errors.New("smtp: server doesn't support AUTH")
}
if err = c.Auth(a); err != nil {
return err
}
}
if err := c.SendMail(from, to, r); err != nil {
return err return err
} }
return c.Quit() return c.Quit()
@ -616,7 +751,7 @@ func (c *Client) Quit() error {
if err != nil { if err != nil {
return err return err
} }
return c.Text.Close() return c.Close()
} }
func parseEnhancedCode(s string) (EnhancedCode, error) { func parseEnhancedCode(s string) (EnhancedCode, error) {
@ -639,9 +774,6 @@ func parseEnhancedCode(s string) (EnhancedCode, error) {
// toSMTPErr converts textproto.Error into SMTPError, parsing // toSMTPErr converts textproto.Error into SMTPError, parsing
// enhanced status code if it is present. // enhanced status code if it is present.
func toSMTPErr(protoErr *textproto.Error) *SMTPError { func toSMTPErr(protoErr *textproto.Error) *SMTPError {
if protoErr == nil {
return nil
}
smtpErr := &SMTPError{ smtpErr := &SMTPError{
Code: protoErr.Code, Code: protoErr.Code,
Message: protoErr.Msg, Message: protoErr.Msg,
@ -677,3 +809,11 @@ func (cdw clientDebugWriter) Write(b []byte) (int, error) {
} }
return cdw.c.DebugWriter.Write(b) return cdw.c.DebugWriter.Write(b)
} }
// validateLine checks to see if a line has CR or LF.
func validateLine(line string) error {
if strings.ContainsAny(line, "\n\r") {
return errors.New("smtp: a line must not contain CR or LF")
}
return nil
}

File diff suppressed because it is too large Load diff

View file

@ -2,6 +2,7 @@ package smtp
import ( import (
"bufio" "bufio"
"fmt"
"io" "io"
) )
@ -29,7 +30,11 @@ var NoEnhancedCode = EnhancedCode{-1, -1, -1}
var EnhancedCodeNotSet = EnhancedCode{0, 0, 0} var EnhancedCodeNotSet = EnhancedCode{0, 0, 0}
func (err *SMTPError) Error() string { func (err *SMTPError) Error() string {
return err.Message s := fmt.Sprintf("SMTP error %03d", err.Code)
if err.Message != "" {
s += ": " + err.Message
}
return s
} }
func (err *SMTPError) Temporary() bool { func (err *SMTPError) Temporary() bool {
@ -77,7 +82,7 @@ func (r *dataReader) Read(b []byte) (n int, err error) {
// not rewrite CRLF -> LF. // not rewrite CRLF -> LF.
// Run data through a simple state machine to // Run data through a simple state machine to
// elide leading dots and detect ending .\r\n line. // elide leading dots and detect End-of-Data (<CR><LF>.<CR><LF>) line.
const ( const (
stateBeginLine = iota // beginning of line; initial state; must be zero stateBeginLine = iota // beginning of line; initial state; must be zero
stateDot // read . at beginning of line stateDot // read . at beginning of line
@ -101,17 +106,16 @@ func (r *dataReader) Read(b []byte) (n int, err error) {
r.state = stateDot r.state = stateDot
continue continue
} }
if c == '\r' {
r.state = stateCR
break
}
r.state = stateData r.state = stateData
case stateDot: case stateDot:
if c == '\r' { if c == '\r' {
r.state = stateDotCR r.state = stateDotCR
continue continue
} }
if c == '\n' {
r.state = stateEOF
continue
}
r.state = stateData r.state = stateData
case stateDotCR: case stateDotCR:
if c == '\n' { if c == '\n' {
@ -129,9 +133,6 @@ func (r *dataReader) Read(b []byte) (n int, err error) {
if c == '\r' { if c == '\r' {
r.state = stateCR r.state = stateCR
} }
if c == '\n' {
r.state = stateBeginLine
}
} }
b[n] = c b[n] = c
n++ n++

View file

@ -5,7 +5,7 @@ import (
"io" "io"
) )
var ErrTooLongLine = errors.New("smtp: too longer line in input stream") var ErrTooLongLine = errors.New("smtp: too long a line in input stream")
// lineLimitReader reads from the underlying Reader but restricts // lineLimitReader reads from the underlying Reader but restricts
// line length of lines in input stream to a certain length. // line length of lines in input stream to a certain length.

View file

@ -5,6 +5,14 @@ import (
"strings" "strings"
) )
// cutPrefixFold is a version of strings.CutPrefix which is case-insensitive.
func cutPrefixFold(s, prefix string) (string, bool) {
if len(s) < len(prefix) || !strings.EqualFold(s[:len(prefix)], prefix) {
return "", false
}
return s[len(prefix):], true
}
func parseCmd(line string) (cmd string, arg string, err error) { func parseCmd(line string) (cmd string, arg string, err error) {
line = strings.TrimRight(line, "\r\n") line = strings.TrimRight(line, "\r\n")
@ -15,36 +23,33 @@ func parseCmd(line string) (cmd string, arg string, err error) {
case l == 0: case l == 0:
return "", "", nil return "", "", nil
case l < 4: case l < 4:
return "", "", fmt.Errorf("Command too short: %q", line) return "", "", fmt.Errorf("command too short: %q", line)
case l == 4: case l == 4:
return strings.ToUpper(line), "", nil return strings.ToUpper(line), "", nil
case l == 5: case l == 5:
// Too long to be only command, too short to have args // Too long to be only command, too short to have args
return "", "", fmt.Errorf("Mangled command: %q", line) return "", "", fmt.Errorf("mangled command: %q", line)
} }
// If we made it here, command is long enough to have args // If we made it here, command is long enough to have args
if line[4] != ' ' { if line[4] != ' ' {
// There wasn't a space after the command? // There wasn't a space after the command?
return "", "", fmt.Errorf("Mangled command: %q", line) return "", "", fmt.Errorf("mangled command: %q", line)
} }
// I'm not sure if we should trim the args or not, but we will for now return strings.ToUpper(line[0:4]), strings.TrimSpace(line[5:]), nil
//return strings.ToUpper(line[0:4]), strings.Trim(line[5:], " "), nil
return strings.ToUpper(line[0:4]), strings.Trim(line[5:], " \n\r"), nil
} }
// Takes the arguments proceeding a command and files them // Takes the arguments proceeding a command and files them
// into a map[string]string after uppercasing each key. Sample arg // into a map[string]string after uppercasing each key. Sample arg
// string: // string:
// " BODY=8BITMIME SIZE=1024 SMTPUTF8" //
// " BODY=8BITMIME SIZE=1024 SMTPUTF8"
//
// The leading space is mandatory. // The leading space is mandatory.
func parseArgs(args []string) (map[string]string, error) { func parseArgs(s string) (map[string]string, error) {
argMap := map[string]string{} argMap := map[string]string{}
for _, arg := range args { for _, arg := range strings.Fields(s) {
if arg == "" {
continue
}
m := strings.Split(arg, "=") m := strings.Split(arg, "=")
switch len(m) { switch len(m) {
case 2: case 2:
@ -52,7 +57,7 @@ func parseArgs(args []string) (map[string]string, error) {
case 1: case 1:
argMap[strings.ToUpper(m[0])] = "" argMap[strings.ToUpper(m[0])] = ""
default: default:
return nil, fmt.Errorf("Failed to parse arg string: %q", arg) return nil, fmt.Errorf("failed to parse arg string: %q", arg)
} }
} }
return argMap, nil return argMap, nil
@ -64,7 +69,146 @@ func parseHelloArgument(arg string) (string, error) {
domain = arg[:idx] domain = arg[:idx]
} }
if domain == "" { if domain == "" {
return "", fmt.Errorf("Invalid domain") return "", fmt.Errorf("invalid domain")
} }
return domain, nil return domain, nil
} }
// parser parses command arguments defined in RFC 5321 section 4.1.2.
type parser struct {
s string
}
func (p *parser) peekByte() (byte, bool) {
if len(p.s) == 0 {
return 0, false
}
return p.s[0], true
}
func (p *parser) readByte() (byte, bool) {
ch, ok := p.peekByte()
if ok {
p.s = p.s[1:]
}
return ch, ok
}
func (p *parser) acceptByte(ch byte) bool {
got, ok := p.peekByte()
if !ok || got != ch {
return false
}
p.readByte()
return true
}
func (p *parser) expectByte(ch byte) error {
if !p.acceptByte(ch) {
if len(p.s) == 0 {
return fmt.Errorf("expected '%v', got EOF", string(ch))
} else {
return fmt.Errorf("expected '%v', got '%v'", string(ch), string(p.s[0]))
}
}
return nil
}
func (p *parser) parseReversePath() (string, error) {
if strings.HasPrefix(p.s, "<>") {
p.s = strings.TrimPrefix(p.s, "<>")
return "", nil
}
return p.parsePath()
}
func (p *parser) parsePath() (string, error) {
hasBracket := p.acceptByte('<')
if p.acceptByte('@') {
i := strings.IndexByte(p.s, ':')
if i < 0 {
return "", fmt.Errorf("malformed a-d-l")
}
p.s = p.s[i+1:]
}
mbox, err := p.parseMailbox()
if err != nil {
return "", fmt.Errorf("in mailbox: %v", err)
}
if hasBracket {
if err := p.expectByte('>'); err != nil {
return "", err
}
}
return mbox, nil
}
func (p *parser) parseMailbox() (string, error) {
localPart, err := p.parseLocalPart()
if err != nil {
return "", fmt.Errorf("in local-part: %v", err)
} else if localPart == "" {
return "", fmt.Errorf("local-part is empty")
}
if err := p.expectByte('@'); err != nil {
return "", err
}
var sb strings.Builder
sb.WriteString(localPart)
sb.WriteByte('@')
for {
ch, ok := p.peekByte()
if !ok {
break
}
if ch == ' ' || ch == '\t' || ch == '>' {
break
}
p.readByte()
sb.WriteByte(ch)
}
if strings.HasSuffix(sb.String(), "@") {
return "", fmt.Errorf("domain is empty")
}
return sb.String(), nil
}
func (p *parser) parseLocalPart() (string, error) {
var sb strings.Builder
if p.acceptByte('"') { // quoted-string
for {
ch, ok := p.readByte()
switch ch {
case '\\':
ch, ok = p.readByte()
case '"':
return sb.String(), nil
}
if !ok {
return "", fmt.Errorf("malformed quoted-string")
}
sb.WriteByte(ch)
}
} else { // dot-string
for {
ch, ok := p.peekByte()
if !ok {
return sb.String(), nil
}
switch ch {
case '@':
return sb.String(), nil
case '(', ')', '<', '>', '[', ']', ':', ';', '\\', ',', '"', ' ', '\t':
return "", fmt.Errorf("malformed dot-string")
}
p.readByte()
sb.WriteByte(ch)
}
}
}

View file

@ -1,6 +1,7 @@
package smtp package smtp
import ( import (
"context"
"crypto/tls" "crypto/tls"
"errors" "errors"
"io" "io"
@ -13,7 +14,9 @@ import (
"github.com/emersion/go-sasl" "github.com/emersion/go-sasl"
) )
var errTCPAndLMTP = errors.New("smtp: cannot start LMTP server listening on a TCP socket") var (
ErrServerClosed = errors.New("smtp: server already closed")
)
// A function that creates SASL servers. // A function that creates SASL servers.
type SaslServerFactory func(conn *Conn) sasl.Server type SaslServerFactory func(conn *Conn) sasl.Server
@ -26,20 +29,20 @@ type Logger interface {
// A SMTP server. // A SMTP server.
type Server struct { type Server struct {
// The type of network, "tcp" or "unix".
Network string
// TCP or Unix address to listen on. // TCP or Unix address to listen on.
Addr string Addr string
// The server TLS configuration. // The server TLS configuration.
TLSConfig *tls.Config TLSConfig *tls.Config
// Enable LMTP mode, as defined in RFC 2033. LMTP mode cannot be used with a // Enable LMTP mode, as defined in RFC 2033.
// TCP listener.
LMTP bool LMTP bool
Domain string Domain string
MaxRecipients int MaxRecipients int
MaxMessageBytes int MaxMessageBytes int64
MaxLineLength int MaxLineLength int
AllowInsecureAuth bool AllowInsecureAuth bool
Strict bool
Debug io.Writer Debug io.Writer
ErrorLog Logger ErrorLog Logger
ReadTimeout time.Duration ReadTimeout time.Duration
@ -57,6 +60,10 @@ type Server struct {
// Should be used only if backend supports it. // Should be used only if backend supports it.
EnableBINARYMIME bool EnableBINARYMIME bool
// Advertise DSN (RFC 3461) capability.
// Should be used only if backend supports it.
EnableDSN bool
// If set, the AUTH command will not be advertised and authentication // If set, the AUTH command will not be advertised and authentication
// attempts will be rejected. This setting overrides AllowInsecureAuth. // attempts will be rejected. This setting overrides AllowInsecureAuth.
AuthDisabled bool AuthDisabled bool
@ -64,6 +71,8 @@ type Server struct {
// The server backend. // The server backend.
Backend Backend Backend Backend
wg sync.WaitGroup
caps []string caps []string
auths map[string]SaslServerFactory auths map[string]SaslServerFactory
done chan struct{} done chan struct{}
@ -87,17 +96,15 @@ func NewServer(be Backend) *Server {
sasl.Plain: func(conn *Conn) sasl.Server { sasl.Plain: func(conn *Conn) sasl.Server {
return sasl.NewPlainServer(func(identity, username, password string) error { return sasl.NewPlainServer(func(identity, username, password string) error {
if identity != "" && identity != username { if identity != "" && identity != username {
return errors.New("Identities not supported") return errors.New("identities not supported")
} }
state := conn.State() sess := conn.Session()
session, err := be.Login(&state, username, password) if sess == nil {
if err != nil { panic("No session when AUTH is called")
return err
} }
conn.SetSession(session) return sess.AuthPlain(username, password)
return nil
}) })
}, },
}, },
@ -111,6 +118,8 @@ func (s *Server) Serve(l net.Listener) error {
s.listeners = append(s.listeners, l) s.listeners = append(s.listeners, l)
s.locker.Unlock() s.locker.Unlock()
var tempDelay time.Duration // how long to sleep on accept failure
for { for {
c, err := l.Accept() c, err := l.Accept()
if err != nil { if err != nil {
@ -119,11 +128,32 @@ func (s *Server) Serve(l net.Listener) error {
// we called Close() // we called Close()
return nil return nil
default: default:
return err
} }
if ne, ok := err.(net.Error); ok && ne.Temporary() {
if tempDelay == 0 {
tempDelay = 5 * time.Millisecond
} else {
tempDelay *= 2
}
if max := 1 * time.Second; tempDelay > max {
tempDelay = max
}
s.ErrorLog.Printf("accept error: %s; retrying in %s", err, tempDelay)
time.Sleep(tempDelay)
continue
}
return err
} }
go s.handleConn(newConn(c, s)) s.wg.Add(1)
go func() {
defer s.wg.Done()
err := s.handleConn(newConn(c, s))
if err != nil {
s.ErrorLog.Printf("handler error: %s", err)
}
}()
} }
} }
@ -140,10 +170,22 @@ func (s *Server) handleConn(c *Conn) error {
s.locker.Unlock() s.locker.Unlock()
}() }()
if tlsConn, ok := c.conn.(*tls.Conn); ok {
if d := s.ReadTimeout; d != 0 {
c.conn.SetReadDeadline(time.Now().Add(d))
}
if d := s.WriteTimeout; d != 0 {
c.conn.SetWriteDeadline(time.Now().Add(d))
}
if err := tlsConn.Handshake(); err != nil {
return err
}
}
c.greet() c.greet()
for { for {
line, err := c.ReadLine() line, err := c.readLine()
if err == nil { if err == nil {
cmd, arg, err := parseCmd(line) cmd, arg, err := parseCmd(line)
if err != nil { if err != nil {
@ -153,34 +195,41 @@ func (s *Server) handleConn(c *Conn) error {
c.handle(cmd, arg) c.handle(cmd, arg)
} else { } else {
if err == io.EOF { if err == io.EOF || errors.Is(err, net.ErrClosed) {
return nil return nil
} }
if err == ErrTooLongLine { if err == ErrTooLongLine {
c.WriteResponse(500, EnhancedCode{5, 4, 0}, "Too long line, closing connection") c.writeResponse(500, EnhancedCode{5, 4, 0}, "Too long line, closing connection")
return nil return nil
} }
if neterr, ok := err.(net.Error); ok && neterr.Timeout() { if neterr, ok := err.(net.Error); ok && neterr.Timeout() {
c.WriteResponse(221, EnhancedCode{2, 4, 2}, "Idle timeout, bye bye") c.writeResponse(421, EnhancedCode{4, 4, 2}, "Idle timeout, bye bye")
return nil return nil
} }
c.WriteResponse(221, EnhancedCode{2, 4, 0}, "Connection error, sorry") c.writeResponse(421, EnhancedCode{4, 4, 0}, "Connection error, sorry")
return err return err
} }
} }
} }
func (s *Server) network() string {
if s.Network != "" {
return s.Network
}
if s.LMTP {
return "unix"
}
return "tcp"
}
// ListenAndServe listens on the network address s.Addr and then calls Serve // ListenAndServe listens on the network address s.Addr and then calls Serve
// to handle requests on incoming connections. // to handle requests on incoming connections.
// //
// If s.Addr is blank and LMTP is disabled, ":smtp" is used. // If s.Addr is blank and LMTP is disabled, ":smtp" is used.
func (s *Server) ListenAndServe() error { func (s *Server) ListenAndServe() error {
network := "tcp" network := s.network()
if s.LMTP {
network = "unix"
}
addr := s.Addr addr := s.Addr
if !s.LMTP && addr == "" { if !s.LMTP && addr == "" {
@ -198,18 +247,16 @@ func (s *Server) ListenAndServe() error {
// ListenAndServeTLS listens on the TCP network address s.Addr and then calls // ListenAndServeTLS listens on the TCP network address s.Addr and then calls
// Serve to handle requests on incoming TLS connections. // Serve to handle requests on incoming TLS connections.
// //
// If s.Addr is blank, ":smtps" is used. // If s.Addr is blank and LMTP is disabled, ":smtps" is used.
func (s *Server) ListenAndServeTLS() error { func (s *Server) ListenAndServeTLS() error {
if s.LMTP { network := s.network()
return errTCPAndLMTP
}
addr := s.Addr addr := s.Addr
if addr == "" { if !s.LMTP && addr == "" {
addr = ":smtps" addr = ":smtps"
} }
l, err := tls.Listen("tcp", addr, s.TLSConfig) l, err := tls.Listen(network, addr, s.TLSConfig)
if err != nil { if err != nil {
return err return err
} }
@ -224,19 +271,19 @@ func (s *Server) ListenAndServeTLS() error {
func (s *Server) Close() error { func (s *Server) Close() error {
select { select {
case <-s.done: case <-s.done:
return errors.New("smtp: server already closed") return ErrServerClosed
default: default:
close(s.done) close(s.done)
} }
var err error var err error
s.locker.Lock()
for _, l := range s.listeners { for _, l := range s.listeners {
if lerr := l.Close(); lerr != nil && err == nil { if lerr := l.Close(); lerr != nil && err == nil {
err = lerr err = lerr
} }
} }
s.locker.Lock()
for conn := range s.conns { for conn := range s.conns {
conn.Close() conn.Close()
} }
@ -245,6 +292,44 @@ func (s *Server) Close() error {
return err return err
} }
// Shutdown gracefully shuts down the server without interrupting any
// active connections. Shutdown works by first closing all open
// listeners and then waiting indefinitely for connections to return to
// idle and then shut down.
// If the provided context expires before the shutdown is complete,
// Shutdown returns the context's error, otherwise it returns any
// error returned from closing the Server's underlying Listener(s).
func (s *Server) Shutdown(ctx context.Context) error {
select {
case <-s.done:
return ErrServerClosed
default:
close(s.done)
}
var err error
s.locker.Lock()
for _, l := range s.listeners {
if lerr := l.Close(); lerr != nil && err == nil {
err = lerr
}
}
s.locker.Unlock()
connDone := make(chan struct{})
go func() {
defer close(connDone)
s.wg.Wait()
}()
select {
case <-ctx.Done():
return ctx.Err()
case <-connDone:
return err
}
}
// EnableAuth enables an authentication mechanism on this server. // EnableAuth enables an authentication mechanism on this server.
// //
// This function should not be called directly, it must only be used by // This function should not be called directly, it must only be used by
@ -252,12 +337,3 @@ func (s *Server) Close() error {
func (s *Server) EnableAuth(name string, f SaslServerFactory) { func (s *Server) EnableAuth(name string, f SaslServerFactory) {
s.auths[name] = f s.auths[name] = f
} }
// ForEachConn iterates through all opened connections.
func (s *Server) ForEachConn(f func(*Conn)) {
s.locker.Lock()
defer s.locker.Unlock()
for conn := range s.conns {
f(conn)
}
}

View file

@ -2,29 +2,93 @@
// //
// It also implements the following extensions: // It also implements the following extensions:
// //
// 8BITMIME: RFC 1652 // - 8BITMIME (RFC 1652)
// AUTH: RFC 2554 // - AUTH (RFC 2554)
// STARTTLS: RFC 3207 // - STARTTLS (RFC 3207)
// ENHANCEDSTATUSCODES: RFC 2034 // - ENHANCEDSTATUSCODES (RFC 2034)
// SMTPUTF8: RFC 6531 // - SMTPUTF8 (RFC 6531)
// REQUIRETLS: RFC 8689 // - REQUIRETLS (RFC 8689)
// CHUNKING: RFC 3030 // - CHUNKING (RFC 3030)
// BINARYMIME: RFC 3030 // - BINARYMIME (RFC 3030)
// - DSN (RFC 3461, RFC 6533)
// //
// LMTP (RFC 2033) is also supported. // LMTP (RFC 2033) is also supported.
// //
// Additional extensions may be handled by other packages. // Additional extensions may be handled by other packages.
package smtp package smtp
import ( type BodyType string
"errors"
"strings" const (
Body7Bit BodyType = "7BIT"
Body8BitMIME BodyType = "8BITMIME"
BodyBinaryMIME BodyType = "BINARYMIME"
) )
// validateLine checks to see if a line has CR or LF as per RFC 5321 type DSNReturn string
func validateLine(line string) error {
if strings.ContainsAny(line, "\n\r") { const (
return errors.New("smtp: A line must not contain CR or LF") DSNReturnFull DSNReturn = "FULL"
} DSNReturnHeaders DSNReturn = "HDRS"
return nil )
// MailOptions contains parameters for the MAIL command.
type MailOptions struct {
// Value of BODY= argument, 7BIT, 8BITMIME or BINARYMIME.
Body BodyType
// Size of the body. Can be 0 if not specified by client.
Size int64
// TLS is required for the message transmission.
//
// The message should be rejected if it can't be transmitted
// with TLS.
RequireTLS bool
// The message envelope or message header contains UTF-8-encoded strings.
// This flag is set by SMTPUTF8-aware (RFC 6531) client.
UTF8 bool
// Value of RET= argument, FULL or HDRS.
Return DSNReturn
// Envelope identifier set by the client.
EnvelopeID string
// The authorization identity asserted by the message sender in decoded
// form with angle brackets stripped.
//
// nil value indicates missing AUTH, non-nil empty string indicates
// AUTH=<>.
//
// Defined in RFC 4954.
Auth *string
}
type DSNNotify string
const (
DSNNotifyNever DSNNotify = "NEVER"
DSNNotifyDelayed DSNNotify = "DELAY"
DSNNotifyFailure DSNNotify = "FAILURE"
DSNNotifySuccess DSNNotify = "SUCCESS"
)
type DSNAddressType string
const (
DSNAddressTypeRFC822 DSNAddressType = "RFC822"
DSNAddressTypeUTF8 DSNAddressType = "UTF-8"
)
// RcptOptions contains parameters for the RCPT command.
type RcptOptions struct {
// Value of NOTIFY= argument, NEVER or a combination of either of
// DELAY, FAILURE, SUCCESS.
Notify []DSNNotify
// Original recipient set by client.
OriginalRecipientType DSNAddressType
OriginalRecipient string
} }

13
vendor/github.com/fsnotify/fsnotify/.cirrus.yml generated vendored Normal file
View file

@ -0,0 +1,13 @@
freebsd_task:
name: 'FreeBSD'
freebsd_instance:
image_family: freebsd-13-2
install_script:
- pkg update -f
- pkg install -y go
test_script:
# run tests as user "cirrus" instead of root
- pw useradd cirrus -m
- chown -R cirrus:cirrus .
- FSNOTIFY_BUFFER=4096 sudo --preserve-env=FSNOTIFY_BUFFER -u cirrus go test -parallel 1 -race ./...
- sudo --preserve-env=FSNOTIFY_BUFFER -u cirrus go test -parallel 1 -race ./...

View file

@ -4,3 +4,4 @@
# Output of go build ./cmd/fsnotify # Output of go build ./cmd/fsnotify
/fsnotify /fsnotify
/fsnotify.exe

View file

@ -1,16 +1,87 @@
# Changelog # Changelog
All notable changes to this project will be documented in this file. Unreleased
----------
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
Nothing yet. Nothing yet.
## [1.6.0] - 2022-10-13 1.7.0 - 2023-10-22
------------------
This version of fsnotify needs Go 1.17.
### Additions
- illumos: add FEN backend to support illumos and Solaris. ([#371])
- all: add `NewBufferedWatcher()` to use a buffered channel, which can be useful
in cases where you can't control the kernel buffer and receive a large number
of events in bursts. ([#550], [#572])
- all: add `AddWith()`, which is identical to `Add()` but allows passing
options. ([#521])
- windows: allow setting the ReadDirectoryChangesW() buffer size with
`fsnotify.WithBufferSize()`; the default of 64K is the highest value that
works on all platforms and is enough for most purposes, but in some cases a
highest buffer is needed. ([#521])
### Changes and fixes
- inotify: remove watcher if a watched path is renamed ([#518])
After a rename the reported name wasn't updated, or even an empty string.
Inotify doesn't provide any good facilities to update it, so just remove the
watcher. This is already how it worked on kqueue and FEN.
On Windows this does work, and remains working.
- windows: don't listen for file attribute changes ([#520])
File attribute changes are sent as `FILE_ACTION_MODIFIED` by the Windows API,
with no way to see if they're a file write or attribute change, so would show
up as a fsnotify.Write event. This is never useful, and could result in many
spurious Write events.
- windows: return `ErrEventOverflow` if the buffer is full ([#525])
Before it would merely return "short read", making it hard to detect this
error.
- kqueue: make sure events for all files are delivered properly when removing a
watched directory ([#526])
Previously they would get sent with `""` (empty string) or `"."` as the path
name.
- kqueue: don't emit spurious Create events for symbolic links ([#524])
The link would get resolved but kqueue would "forget" it already saw the link
itself, resulting on a Create for every Write event for the directory.
- all: return `ErrClosed` on `Add()` when the watcher is closed ([#516])
- other: add `Watcher.Errors` and `Watcher.Events` to the no-op `Watcher` in
`backend_other.go`, making it easier to use on unsupported platforms such as
WASM, AIX, etc. ([#528])
- other: use the `backend_other.go` no-op if the `appengine` build tag is set;
Google AppEngine forbids usage of the unsafe package so the inotify backend
won't compile there.
[#371]: https://github.com/fsnotify/fsnotify/pull/371
[#516]: https://github.com/fsnotify/fsnotify/pull/516
[#518]: https://github.com/fsnotify/fsnotify/pull/518
[#520]: https://github.com/fsnotify/fsnotify/pull/520
[#521]: https://github.com/fsnotify/fsnotify/pull/521
[#524]: https://github.com/fsnotify/fsnotify/pull/524
[#525]: https://github.com/fsnotify/fsnotify/pull/525
[#526]: https://github.com/fsnotify/fsnotify/pull/526
[#528]: https://github.com/fsnotify/fsnotify/pull/528
[#537]: https://github.com/fsnotify/fsnotify/pull/537
[#550]: https://github.com/fsnotify/fsnotify/pull/550
[#572]: https://github.com/fsnotify/fsnotify/pull/572
1.6.0 - 2022-10-13
------------------
This version of fsnotify needs Go 1.16 (this was already the case since 1.5.1, This version of fsnotify needs Go 1.16 (this was already the case since 1.5.1,
but not documented). It also increases the minimum Linux version to 2.6.32. but not documented). It also increases the minimum Linux version to 2.6.32.

View file

@ -1,29 +1,31 @@
fsnotify is a Go library to provide cross-platform filesystem notifications on fsnotify is a Go library to provide cross-platform filesystem notifications on
Windows, Linux, macOS, and BSD systems. Windows, Linux, macOS, BSD, and illumos.
Go 1.16 or newer is required; the full documentation is at Go 1.17 or newer is required; the full documentation is at
https://pkg.go.dev/github.com/fsnotify/fsnotify https://pkg.go.dev/github.com/fsnotify/fsnotify
**It's best to read the documentation at pkg.go.dev, as it's pinned to the last
released version, whereas this README is for the last development version which
may include additions/changes.**
--- ---
Platform support: Platform support:
| Adapter | OS | Status | | Backend | OS | Status |
| --------------------- | ---------------| -------------------------------------------------------------| | :-------------------- | :--------- | :------------------------------------------------------------------------ |
| inotify | Linux 2.6.32+ | Supported | | inotify | Linux | Supported |
| kqueue | BSD, macOS | Supported | | kqueue | BSD, macOS | Supported |
| ReadDirectoryChangesW | Windows | Supported | | ReadDirectoryChangesW | Windows | Supported |
| FSEvents | macOS | [Planned](https://github.com/fsnotify/fsnotify/issues/11) | | FEN | illumos | Supported |
| FEN | Solaris 11 | [In Progress](https://github.com/fsnotify/fsnotify/pull/371) | | fanotify | Linux 5.9+ | [Not yet](https://github.com/fsnotify/fsnotify/issues/114) |
| fanotify | Linux 5.9+ | [Maybe](https://github.com/fsnotify/fsnotify/issues/114) | | AHAFS | AIX | [aix branch]; experimental due to lack of maintainer and test environment |
| USN Journals | Windows | [Maybe](https://github.com/fsnotify/fsnotify/issues/53) | | FSEvents | macOS | [Needs support in x/sys/unix][fsevents] |
| Polling | *All* | [Maybe](https://github.com/fsnotify/fsnotify/issues/9) | | USN Journals | Windows | [Needs support in x/sys/windows][usn] |
| Polling | *All* | [Not yet](https://github.com/fsnotify/fsnotify/issues/9) |
Linux and macOS should include Android and iOS, but these are currently untested. Linux and illumos should include Android and Solaris, but these are currently
untested.
[fsevents]: https://github.com/fsnotify/fsnotify/issues/11#issuecomment-1279133120
[usn]: https://github.com/fsnotify/fsnotify/issues/53#issuecomment-1279829847
[aix branch]: https://github.com/fsnotify/fsnotify/issues/353#issuecomment-1284590129
Usage Usage
----- -----
@ -83,20 +85,23 @@ run with:
% go run ./cmd/fsnotify % go run ./cmd/fsnotify
Further detailed documentation can be found in godoc:
https://pkg.go.dev/github.com/fsnotify/fsnotify
FAQ FAQ
--- ---
### Will a file still be watched when it's moved to another directory? ### Will a file still be watched when it's moved to another directory?
No, not unless you are watching the location it was moved to. No, not unless you are watching the location it was moved to.
### Are subdirectories watched too? ### Are subdirectories watched?
No, you must add watches for any directory you want to watch (a recursive No, you must add watches for any directory you want to watch (a recursive
watcher is on the roadmap: [#18]). watcher is on the roadmap: [#18]).
[#18]: https://github.com/fsnotify/fsnotify/issues/18 [#18]: https://github.com/fsnotify/fsnotify/issues/18
### Do I have to watch the Error and Event channels in a goroutine? ### Do I have to watch the Error and Event channels in a goroutine?
As of now, yes (you can read both channels in the same goroutine using `select`, Yes. You can read both channels in the same goroutine using `select` (you don't
you don't need a separate goroutine for both channels; see the example). need a separate goroutine for both channels; see the example).
### Why don't notifications work with NFS, SMB, FUSE, /proc, or /sys? ### Why don't notifications work with NFS, SMB, FUSE, /proc, or /sys?
fsnotify requires support from underlying OS to work. The current NFS and SMB fsnotify requires support from underlying OS to work. The current NFS and SMB
@ -107,6 +112,32 @@ This could be fixed with a polling watcher ([#9]), but it's not yet implemented.
[#9]: https://github.com/fsnotify/fsnotify/issues/9 [#9]: https://github.com/fsnotify/fsnotify/issues/9
### Why do I get many Chmod events?
Some programs may generate a lot of attribute changes; for example Spotlight on
macOS, anti-virus programs, backup applications, and some others are known to do
this. As a rule, it's typically best to ignore Chmod events. They're often not
useful, and tend to cause problems.
Spotlight indexing on macOS can result in multiple events (see [#15]). A
temporary workaround is to add your folder(s) to the *Spotlight Privacy
settings* until we have a native FSEvents implementation (see [#11]).
[#11]: https://github.com/fsnotify/fsnotify/issues/11
[#15]: https://github.com/fsnotify/fsnotify/issues/15
### Watching a file doesn't work well
Watching individual files (rather than directories) is generally not recommended
as many programs (especially editors) update files atomically: it will write to
a temporary file which is then moved to to destination, overwriting the original
(or some variant thereof). The watcher on the original file is now lost, as that
no longer exists.
The upshot of this is that a power failure or crash won't leave a half-written
file.
Watch the parent directory and use `Event.Name` to filter out files you're not
interested in. There is an example of this in `cmd/fsnotify/file.go`.
Platform-specific notes Platform-specific notes
----------------------- -----------------------
### Linux ### Linux
@ -151,11 +182,3 @@ these platforms.
The sysctl variables `kern.maxfiles` and `kern.maxfilesperproc` can be used to The sysctl variables `kern.maxfiles` and `kern.maxfilesperproc` can be used to
control the maximum number of open files. control the maximum number of open files.
### macOS
Spotlight indexing on macOS can result in multiple events (see [#15]). A temporary
workaround is to add your folder(s) to the *Spotlight Privacy settings* until we
have a native FSEvents implementation (see [#11]).
[#11]: https://github.com/fsnotify/fsnotify/issues/11
[#15]: https://github.com/fsnotify/fsnotify/issues/15

View file

@ -1,10 +1,19 @@
//go:build solaris //go:build solaris
// +build solaris // +build solaris
// Note: the documentation on the Watcher type and methods is generated from
// mkdoc.zsh
package fsnotify package fsnotify
import ( import (
"errors" "errors"
"fmt"
"os"
"path/filepath"
"sync"
"golang.org/x/sys/unix"
) )
// Watcher watches a set of paths, delivering events on a channel. // Watcher watches a set of paths, delivering events on a channel.
@ -17,9 +26,9 @@ import (
// When a file is removed a Remove event won't be emitted until all file // When a file is removed a Remove event won't be emitted until all file
// descriptors are closed, and deletes will always emit a Chmod. For example: // descriptors are closed, and deletes will always emit a Chmod. For example:
// //
// fp := os.Open("file") // fp := os.Open("file")
// os.Remove("file") // Triggers Chmod // os.Remove("file") // Triggers Chmod
// fp.Close() // Triggers Remove // fp.Close() // Triggers Remove
// //
// This is the event that inotify sends, so not much can be changed about this. // This is the event that inotify sends, so not much can be changed about this.
// //
@ -33,16 +42,16 @@ import (
// //
// To increase them you can use sysctl or write the value to the /proc file: // To increase them you can use sysctl or write the value to the /proc file:
// //
// # Default values on Linux 5.18 // # Default values on Linux 5.18
// sysctl fs.inotify.max_user_watches=124983 // sysctl fs.inotify.max_user_watches=124983
// sysctl fs.inotify.max_user_instances=128 // sysctl fs.inotify.max_user_instances=128
// //
// To make the changes persist on reboot edit /etc/sysctl.conf or // To make the changes persist on reboot edit /etc/sysctl.conf or
// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check // /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
// your distro's documentation): // your distro's documentation):
// //
// fs.inotify.max_user_watches=124983 // fs.inotify.max_user_watches=124983
// fs.inotify.max_user_instances=128 // fs.inotify.max_user_instances=128
// //
// Reaching the limit will result in a "no space left on device" or "too many open // Reaching the limit will result in a "no space left on device" or "too many open
// files" error. // files" error.
@ -58,14 +67,20 @@ import (
// control the maximum number of open files, as well as /etc/login.conf on BSD // control the maximum number of open files, as well as /etc/login.conf on BSD
// systems. // systems.
// //
// # macOS notes // # Windows notes
// //
// Spotlight indexing on macOS can result in multiple events (see [#15]). A // Paths can be added as "C:\path\to\dir", but forward slashes
// temporary workaround is to add your folder(s) to the "Spotlight Privacy // ("C:/path/to/dir") will also work.
// Settings" until we have a native FSEvents implementation (see [#11]).
// //
// [#11]: https://github.com/fsnotify/fsnotify/issues/11 // When a watched directory is removed it will always send an event for the
// [#15]: https://github.com/fsnotify/fsnotify/issues/15 // directory itself, but may not send events for all files in that directory.
// Sometimes it will send events for all times, sometimes it will send no
// events, and often only for some files.
//
// The default ReadDirectoryChangesW() buffer size is 64K, which is the largest
// value that is guaranteed to work with SMB filesystems. If you have many
// events in quick succession this may not be enough, and you will have to use
// [WithBufferSize] to increase the value.
type Watcher struct { type Watcher struct {
// Events sends the filesystem change events. // Events sends the filesystem change events.
// //
@ -92,44 +107,129 @@ type Watcher struct {
// initiated by the user may show up as one or multiple // initiated by the user may show up as one or multiple
// writes, depending on when the system syncs things to // writes, depending on when the system syncs things to
// disk. For example when compiling a large Go program // disk. For example when compiling a large Go program
// you may get hundreds of Write events, so you // you may get hundreds of Write events, and you may
// probably want to wait until you've stopped receiving // want to wait until you've stopped receiving them
// them (see the dedup example in cmd/fsnotify). // (see the dedup example in cmd/fsnotify).
//
// Some systems may send Write event for directories
// when the directory content changes.
// //
// fsnotify.Chmod Attributes were changed. On Linux this is also sent // fsnotify.Chmod Attributes were changed. On Linux this is also sent
// when a file is removed (or more accurately, when a // when a file is removed (or more accurately, when a
// link to an inode is removed). On kqueue it's sent // link to an inode is removed). On kqueue it's sent
// and on kqueue when a file is truncated. On Windows // when a file is truncated. On Windows it's never
// it's never sent. // sent.
Events chan Event Events chan Event
// Errors sends any errors. // Errors sends any errors.
//
// ErrEventOverflow is used to indicate there are too many events:
//
// - inotify: There are too many queued events (fs.inotify.max_queued_events sysctl)
// - windows: The buffer size is too small; WithBufferSize() can be used to increase it.
// - kqueue, fen: Not used.
Errors chan error Errors chan error
mu sync.Mutex
port *unix.EventPort
done chan struct{} // Channel for sending a "quit message" to the reader goroutine
dirs map[string]struct{} // Explicitly watched directories
watches map[string]struct{} // Explicitly watched non-directories
} }
// NewWatcher creates a new Watcher. // NewWatcher creates a new Watcher.
func NewWatcher() (*Watcher, error) { func NewWatcher() (*Watcher, error) {
return nil, errors.New("FEN based watcher not yet supported for fsnotify\n") return NewBufferedWatcher(0)
} }
// Close removes all watches and closes the events channel. // NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events
// channel.
//
// The main use case for this is situations with a very large number of events
// where the kernel buffer size can't be increased (e.g. due to lack of
// permissions). An unbuffered Watcher will perform better for almost all use
// cases, and whenever possible you will be better off increasing the kernel
// buffers instead of adding a large userspace buffer.
func NewBufferedWatcher(sz uint) (*Watcher, error) {
w := &Watcher{
Events: make(chan Event, sz),
Errors: make(chan error),
dirs: make(map[string]struct{}),
watches: make(map[string]struct{}),
done: make(chan struct{}),
}
var err error
w.port, err = unix.NewEventPort()
if err != nil {
return nil, fmt.Errorf("fsnotify.NewWatcher: %w", err)
}
go w.readEvents()
return w, nil
}
// sendEvent attempts to send an event to the user, returning true if the event
// was put in the channel successfully and false if the watcher has been closed.
func (w *Watcher) sendEvent(name string, op Op) (sent bool) {
select {
case w.Events <- Event{Name: name, Op: op}:
return true
case <-w.done:
return false
}
}
// sendError attempts to send an error to the user, returning true if the error
// was put in the channel successfully and false if the watcher has been closed.
func (w *Watcher) sendError(err error) (sent bool) {
select {
case w.Errors <- err:
return true
case <-w.done:
return false
}
}
func (w *Watcher) isClosed() bool {
select {
case <-w.done:
return true
default:
return false
}
}
// Close removes all watches and closes the Events channel.
func (w *Watcher) Close() error { func (w *Watcher) Close() error {
return nil // Take the lock used by associateFile to prevent lingering events from
// being processed after the close
w.mu.Lock()
defer w.mu.Unlock()
if w.isClosed() {
return nil
}
close(w.done)
return w.port.Close()
} }
// Add starts monitoring the path for changes. // Add starts monitoring the path for changes.
// //
// A path can only be watched once; attempting to watch it more than once will // A path can only be watched once; watching it more than once is a no-op and will
// return an error. Paths that do not yet exist on the filesystem cannot be // not return an error. Paths that do not yet exist on the filesystem cannot be
// added. A watch will be automatically removed if the path is deleted. // watched.
// //
// A path will remain watched if it gets renamed to somewhere else on the same // A watch will be automatically removed if the watched path is deleted or
// filesystem, but the monitor will get removed if the path gets deleted and // renamed. The exception is the Windows backend, which doesn't remove the
// re-created, or if it's moved to a different filesystem. // watcher on renames.
// //
// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special // Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
// filesystems (/proc, /sys, etc.) generally don't work. // filesystems (/proc, /sys, etc.) generally don't work.
// //
// Returns [ErrClosed] if [Watcher.Close] was called.
//
// See [Watcher.AddWith] for a version that allows adding options.
//
// # Watching directories // # Watching directories
// //
// All files in a directory are monitored, including new files that are created // All files in a directory are monitored, including new files that are created
@ -139,15 +239,63 @@ func (w *Watcher) Close() error {
// # Watching files // # Watching files
// //
// Watching individual files (rather than directories) is generally not // Watching individual files (rather than directories) is generally not
// recommended as many tools update files atomically. Instead of "just" writing // recommended as many programs (especially editors) update files atomically: it
// to the file a temporary file will be written to first, and if successful the // will write to a temporary file which is then moved to to destination,
// temporary file is moved to to destination removing the original, or some // overwriting the original (or some variant thereof). The watcher on the
// variant thereof. The watcher on the original file is now lost, as it no // original file is now lost, as that no longer exists.
// longer exists.
// //
// Instead, watch the parent directory and use Event.Name to filter out files // The upshot of this is that a power failure or crash won't leave a
// you're not interested in. There is an example of this in [cmd/fsnotify/file.go]. // half-written file.
func (w *Watcher) Add(name string) error { //
// Watch the parent directory and use Event.Name to filter out files you're not
// interested in. There is an example of this in cmd/fsnotify/file.go.
func (w *Watcher) Add(name string) error { return w.AddWith(name) }
// AddWith is like [Watcher.Add], but allows adding options. When using Add()
// the defaults described below are used.
//
// Possible options are:
//
// - [WithBufferSize] sets the buffer size for the Windows backend; no-op on
// other platforms. The default is 64K (65536 bytes).
func (w *Watcher) AddWith(name string, opts ...addOpt) error {
if w.isClosed() {
return ErrClosed
}
if w.port.PathIsWatched(name) {
return nil
}
_ = getOptions(opts...)
// Currently we resolve symlinks that were explicitly requested to be
// watched. Otherwise we would use LStat here.
stat, err := os.Stat(name)
if err != nil {
return err
}
// Associate all files in the directory.
if stat.IsDir() {
err := w.handleDirectory(name, stat, true, w.associateFile)
if err != nil {
return err
}
w.mu.Lock()
w.dirs[name] = struct{}{}
w.mu.Unlock()
return nil
}
err = w.associateFile(name, stat, true)
if err != nil {
return err
}
w.mu.Lock()
w.watches[name] = struct{}{}
w.mu.Unlock()
return nil return nil
} }
@ -157,6 +305,336 @@ func (w *Watcher) Add(name string) error {
// /tmp/dir and /tmp/dir/subdir then you will need to remove both. // /tmp/dir and /tmp/dir/subdir then you will need to remove both.
// //
// Removing a path that has not yet been added returns [ErrNonExistentWatch]. // Removing a path that has not yet been added returns [ErrNonExistentWatch].
//
// Returns nil if [Watcher.Close] was called.
func (w *Watcher) Remove(name string) error { func (w *Watcher) Remove(name string) error {
if w.isClosed() {
return nil
}
if !w.port.PathIsWatched(name) {
return fmt.Errorf("%w: %s", ErrNonExistentWatch, name)
}
// The user has expressed an intent. Immediately remove this name from
// whichever watch list it might be in. If it's not in there the delete
// doesn't cause harm.
w.mu.Lock()
delete(w.watches, name)
delete(w.dirs, name)
w.mu.Unlock()
stat, err := os.Stat(name)
if err != nil {
return err
}
// Remove associations for every file in the directory.
if stat.IsDir() {
err := w.handleDirectory(name, stat, false, w.dissociateFile)
if err != nil {
return err
}
return nil
}
err = w.port.DissociatePath(name)
if err != nil {
return err
}
return nil return nil
} }
// readEvents contains the main loop that runs in a goroutine watching for events.
func (w *Watcher) readEvents() {
// If this function returns, the watcher has been closed and we can close
// these channels
defer func() {
close(w.Errors)
close(w.Events)
}()
pevents := make([]unix.PortEvent, 8)
for {
count, err := w.port.Get(pevents, 1, nil)
if err != nil && err != unix.ETIME {
// Interrupted system call (count should be 0) ignore and continue
if errors.Is(err, unix.EINTR) && count == 0 {
continue
}
// Get failed because we called w.Close()
if errors.Is(err, unix.EBADF) && w.isClosed() {
return
}
// There was an error not caused by calling w.Close()
if !w.sendError(err) {
return
}
}
p := pevents[:count]
for _, pevent := range p {
if pevent.Source != unix.PORT_SOURCE_FILE {
// Event from unexpected source received; should never happen.
if !w.sendError(errors.New("Event from unexpected source received")) {
return
}
continue
}
err = w.handleEvent(&pevent)
if err != nil {
if !w.sendError(err) {
return
}
}
}
}
}
func (w *Watcher) handleDirectory(path string, stat os.FileInfo, follow bool, handler func(string, os.FileInfo, bool) error) error {
files, err := os.ReadDir(path)
if err != nil {
return err
}
// Handle all children of the directory.
for _, entry := range files {
finfo, err := entry.Info()
if err != nil {
return err
}
err = handler(filepath.Join(path, finfo.Name()), finfo, false)
if err != nil {
return err
}
}
// And finally handle the directory itself.
return handler(path, stat, follow)
}
// handleEvent might need to emit more than one fsnotify event if the events
// bitmap matches more than one event type (e.g. the file was both modified and
// had the attributes changed between when the association was created and the
// when event was returned)
func (w *Watcher) handleEvent(event *unix.PortEvent) error {
var (
events = event.Events
path = event.Path
fmode = event.Cookie.(os.FileMode)
reRegister = true
)
w.mu.Lock()
_, watchedDir := w.dirs[path]
_, watchedPath := w.watches[path]
w.mu.Unlock()
isWatched := watchedDir || watchedPath
if events&unix.FILE_DELETE != 0 {
if !w.sendEvent(path, Remove) {
return nil
}
reRegister = false
}
if events&unix.FILE_RENAME_FROM != 0 {
if !w.sendEvent(path, Rename) {
return nil
}
// Don't keep watching the new file name
reRegister = false
}
if events&unix.FILE_RENAME_TO != 0 {
// We don't report a Rename event for this case, because Rename events
// are interpreted as referring to the _old_ name of the file, and in
// this case the event would refer to the new name of the file. This
// type of rename event is not supported by fsnotify.
// inotify reports a Remove event in this case, so we simulate this
// here.
if !w.sendEvent(path, Remove) {
return nil
}
// Don't keep watching the file that was removed
reRegister = false
}
// The file is gone, nothing left to do.
if !reRegister {
if watchedDir {
w.mu.Lock()
delete(w.dirs, path)
w.mu.Unlock()
}
if watchedPath {
w.mu.Lock()
delete(w.watches, path)
w.mu.Unlock()
}
return nil
}
// If we didn't get a deletion the file still exists and we're going to have
// to watch it again. Let's Stat it now so that we can compare permissions
// and have what we need to continue watching the file
stat, err := os.Lstat(path)
if err != nil {
// This is unexpected, but we should still emit an event. This happens
// most often on "rm -r" of a subdirectory inside a watched directory We
// get a modify event of something happening inside, but by the time we
// get here, the sudirectory is already gone. Clearly we were watching
// this path but now it is gone. Let's tell the user that it was
// removed.
if !w.sendEvent(path, Remove) {
return nil
}
// Suppress extra write events on removed directories; they are not
// informative and can be confusing.
return nil
}
// resolve symlinks that were explicitly watched as we would have at Add()
// time. this helps suppress spurious Chmod events on watched symlinks
if isWatched {
stat, err = os.Stat(path)
if err != nil {
// The symlink still exists, but the target is gone. Report the
// Remove similar to above.
if !w.sendEvent(path, Remove) {
return nil
}
// Don't return the error
}
}
if events&unix.FILE_MODIFIED != 0 {
if fmode.IsDir() {
if watchedDir {
if err := w.updateDirectory(path); err != nil {
return err
}
} else {
if !w.sendEvent(path, Write) {
return nil
}
}
} else {
if !w.sendEvent(path, Write) {
return nil
}
}
}
if events&unix.FILE_ATTRIB != 0 && stat != nil {
// Only send Chmod if perms changed
if stat.Mode().Perm() != fmode.Perm() {
if !w.sendEvent(path, Chmod) {
return nil
}
}
}
if stat != nil {
// If we get here, it means we've hit an event above that requires us to
// continue watching the file or directory
return w.associateFile(path, stat, isWatched)
}
return nil
}
func (w *Watcher) updateDirectory(path string) error {
// The directory was modified, so we must find unwatched entities and watch
// them. If something was removed from the directory, nothing will happen,
// as everything else should still be watched.
files, err := os.ReadDir(path)
if err != nil {
return err
}
for _, entry := range files {
path := filepath.Join(path, entry.Name())
if w.port.PathIsWatched(path) {
continue
}
finfo, err := entry.Info()
if err != nil {
return err
}
err = w.associateFile(path, finfo, false)
if err != nil {
if !w.sendError(err) {
return nil
}
}
if !w.sendEvent(path, Create) {
return nil
}
}
return nil
}
func (w *Watcher) associateFile(path string, stat os.FileInfo, follow bool) error {
if w.isClosed() {
return ErrClosed
}
// This is primarily protecting the call to AssociatePath but it is
// important and intentional that the call to PathIsWatched is also
// protected by this mutex. Without this mutex, AssociatePath has been seen
// to error out that the path is already associated.
w.mu.Lock()
defer w.mu.Unlock()
if w.port.PathIsWatched(path) {
// Remove the old association in favor of this one If we get ENOENT,
// then while the x/sys/unix wrapper still thought that this path was
// associated, the underlying event port did not. This call will have
// cleared up that discrepancy. The most likely cause is that the event
// has fired but we haven't processed it yet.
err := w.port.DissociatePath(path)
if err != nil && err != unix.ENOENT {
return err
}
}
// FILE_NOFOLLOW means we watch symlinks themselves rather than their
// targets.
events := unix.FILE_MODIFIED | unix.FILE_ATTRIB | unix.FILE_NOFOLLOW
if follow {
// We *DO* follow symlinks for explicitly watched entries.
events = unix.FILE_MODIFIED | unix.FILE_ATTRIB
}
return w.port.AssociatePath(path, stat,
events,
stat.Mode())
}
func (w *Watcher) dissociateFile(path string, stat os.FileInfo, unused bool) error {
if !w.port.PathIsWatched(path) {
return nil
}
return w.port.DissociatePath(path)
}
// WatchList returns all paths explicitly added with [Watcher.Add] (and are not
// yet removed).
//
// Returns nil if [Watcher.Close] was called.
func (w *Watcher) WatchList() []string {
if w.isClosed() {
return nil
}
w.mu.Lock()
defer w.mu.Unlock()
entries := make([]string, 0, len(w.watches)+len(w.dirs))
for pathname := range w.dirs {
entries = append(entries, pathname)
}
for pathname := range w.watches {
entries = append(entries, pathname)
}
return entries
}

View file

@ -1,5 +1,8 @@
//go:build linux //go:build linux && !appengine
// +build linux // +build linux,!appengine
// Note: the documentation on the Watcher type and methods is generated from
// mkdoc.zsh
package fsnotify package fsnotify
@ -26,9 +29,9 @@ import (
// When a file is removed a Remove event won't be emitted until all file // When a file is removed a Remove event won't be emitted until all file
// descriptors are closed, and deletes will always emit a Chmod. For example: // descriptors are closed, and deletes will always emit a Chmod. For example:
// //
// fp := os.Open("file") // fp := os.Open("file")
// os.Remove("file") // Triggers Chmod // os.Remove("file") // Triggers Chmod
// fp.Close() // Triggers Remove // fp.Close() // Triggers Remove
// //
// This is the event that inotify sends, so not much can be changed about this. // This is the event that inotify sends, so not much can be changed about this.
// //
@ -42,16 +45,16 @@ import (
// //
// To increase them you can use sysctl or write the value to the /proc file: // To increase them you can use sysctl or write the value to the /proc file:
// //
// # Default values on Linux 5.18 // # Default values on Linux 5.18
// sysctl fs.inotify.max_user_watches=124983 // sysctl fs.inotify.max_user_watches=124983
// sysctl fs.inotify.max_user_instances=128 // sysctl fs.inotify.max_user_instances=128
// //
// To make the changes persist on reboot edit /etc/sysctl.conf or // To make the changes persist on reboot edit /etc/sysctl.conf or
// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check // /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
// your distro's documentation): // your distro's documentation):
// //
// fs.inotify.max_user_watches=124983 // fs.inotify.max_user_watches=124983
// fs.inotify.max_user_instances=128 // fs.inotify.max_user_instances=128
// //
// Reaching the limit will result in a "no space left on device" or "too many open // Reaching the limit will result in a "no space left on device" or "too many open
// files" error. // files" error.
@ -67,14 +70,20 @@ import (
// control the maximum number of open files, as well as /etc/login.conf on BSD // control the maximum number of open files, as well as /etc/login.conf on BSD
// systems. // systems.
// //
// # macOS notes // # Windows notes
// //
// Spotlight indexing on macOS can result in multiple events (see [#15]). A // Paths can be added as "C:\path\to\dir", but forward slashes
// temporary workaround is to add your folder(s) to the "Spotlight Privacy // ("C:/path/to/dir") will also work.
// Settings" until we have a native FSEvents implementation (see [#11]).
// //
// [#11]: https://github.com/fsnotify/fsnotify/issues/11 // When a watched directory is removed it will always send an event for the
// [#15]: https://github.com/fsnotify/fsnotify/issues/15 // directory itself, but may not send events for all files in that directory.
// Sometimes it will send events for all times, sometimes it will send no
// events, and often only for some files.
//
// The default ReadDirectoryChangesW() buffer size is 64K, which is the largest
// value that is guaranteed to work with SMB filesystems. If you have many
// events in quick succession this may not be enough, and you will have to use
// [WithBufferSize] to increase the value.
type Watcher struct { type Watcher struct {
// Events sends the filesystem change events. // Events sends the filesystem change events.
// //
@ -101,36 +110,148 @@ type Watcher struct {
// initiated by the user may show up as one or multiple // initiated by the user may show up as one or multiple
// writes, depending on when the system syncs things to // writes, depending on when the system syncs things to
// disk. For example when compiling a large Go program // disk. For example when compiling a large Go program
// you may get hundreds of Write events, so you // you may get hundreds of Write events, and you may
// probably want to wait until you've stopped receiving // want to wait until you've stopped receiving them
// them (see the dedup example in cmd/fsnotify). // (see the dedup example in cmd/fsnotify).
//
// Some systems may send Write event for directories
// when the directory content changes.
// //
// fsnotify.Chmod Attributes were changed. On Linux this is also sent // fsnotify.Chmod Attributes were changed. On Linux this is also sent
// when a file is removed (or more accurately, when a // when a file is removed (or more accurately, when a
// link to an inode is removed). On kqueue it's sent // link to an inode is removed). On kqueue it's sent
// and on kqueue when a file is truncated. On Windows // when a file is truncated. On Windows it's never
// it's never sent. // sent.
Events chan Event Events chan Event
// Errors sends any errors. // Errors sends any errors.
//
// ErrEventOverflow is used to indicate there are too many events:
//
// - inotify: There are too many queued events (fs.inotify.max_queued_events sysctl)
// - windows: The buffer size is too small; WithBufferSize() can be used to increase it.
// - kqueue, fen: Not used.
Errors chan error Errors chan error
// Store fd here as os.File.Read() will no longer return on close after // Store fd here as os.File.Read() will no longer return on close after
// calling Fd(). See: https://github.com/golang/go/issues/26439 // calling Fd(). See: https://github.com/golang/go/issues/26439
fd int fd int
mu sync.Mutex // Map access
inotifyFile *os.File inotifyFile *os.File
watches map[string]*watch // Map of inotify watches (key: path) watches *watches
paths map[int]string // Map of watched paths (key: watch descriptor) done chan struct{} // Channel for sending a "quit message" to the reader goroutine
done chan struct{} // Channel for sending a "quit message" to the reader goroutine closeMu sync.Mutex
doneResp chan struct{} // Channel to respond to Close doneResp chan struct{} // Channel to respond to Close
}
type (
watches struct {
mu sync.RWMutex
wd map[uint32]*watch // wd → watch
path map[string]uint32 // pathname → wd
}
watch struct {
wd uint32 // Watch descriptor (as returned by the inotify_add_watch() syscall)
flags uint32 // inotify flags of this watch (see inotify(7) for the list of valid flags)
path string // Watch path.
}
)
func newWatches() *watches {
return &watches{
wd: make(map[uint32]*watch),
path: make(map[string]uint32),
}
}
func (w *watches) len() int {
w.mu.RLock()
defer w.mu.RUnlock()
return len(w.wd)
}
func (w *watches) add(ww *watch) {
w.mu.Lock()
defer w.mu.Unlock()
w.wd[ww.wd] = ww
w.path[ww.path] = ww.wd
}
func (w *watches) remove(wd uint32) {
w.mu.Lock()
defer w.mu.Unlock()
delete(w.path, w.wd[wd].path)
delete(w.wd, wd)
}
func (w *watches) removePath(path string) (uint32, bool) {
w.mu.Lock()
defer w.mu.Unlock()
wd, ok := w.path[path]
if !ok {
return 0, false
}
delete(w.path, path)
delete(w.wd, wd)
return wd, true
}
func (w *watches) byPath(path string) *watch {
w.mu.RLock()
defer w.mu.RUnlock()
return w.wd[w.path[path]]
}
func (w *watches) byWd(wd uint32) *watch {
w.mu.RLock()
defer w.mu.RUnlock()
return w.wd[wd]
}
func (w *watches) updatePath(path string, f func(*watch) (*watch, error)) error {
w.mu.Lock()
defer w.mu.Unlock()
var existing *watch
wd, ok := w.path[path]
if ok {
existing = w.wd[wd]
}
upd, err := f(existing)
if err != nil {
return err
}
if upd != nil {
w.wd[upd.wd] = upd
w.path[upd.path] = upd.wd
if upd.wd != wd {
delete(w.wd, wd)
}
}
return nil
} }
// NewWatcher creates a new Watcher. // NewWatcher creates a new Watcher.
func NewWatcher() (*Watcher, error) { func NewWatcher() (*Watcher, error) {
// Create inotify fd return NewBufferedWatcher(0)
// Need to set the FD to nonblocking mode in order for SetDeadline methods to work }
// Otherwise, blocking i/o operations won't terminate on close
// NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events
// channel.
//
// The main use case for this is situations with a very large number of events
// where the kernel buffer size can't be increased (e.g. due to lack of
// permissions). An unbuffered Watcher will perform better for almost all use
// cases, and whenever possible you will be better off increasing the kernel
// buffers instead of adding a large userspace buffer.
func NewBufferedWatcher(sz uint) (*Watcher, error) {
// Need to set nonblocking mode for SetDeadline to work, otherwise blocking
// I/O operations won't terminate on close.
fd, errno := unix.InotifyInit1(unix.IN_CLOEXEC | unix.IN_NONBLOCK) fd, errno := unix.InotifyInit1(unix.IN_CLOEXEC | unix.IN_NONBLOCK)
if fd == -1 { if fd == -1 {
return nil, errno return nil, errno
@ -139,9 +260,8 @@ func NewWatcher() (*Watcher, error) {
w := &Watcher{ w := &Watcher{
fd: fd, fd: fd,
inotifyFile: os.NewFile(uintptr(fd), ""), inotifyFile: os.NewFile(uintptr(fd), ""),
watches: make(map[string]*watch), watches: newWatches(),
paths: make(map[int]string), Events: make(chan Event, sz),
Events: make(chan Event),
Errors: make(chan error), Errors: make(chan error),
done: make(chan struct{}), done: make(chan struct{}),
doneResp: make(chan struct{}), doneResp: make(chan struct{}),
@ -157,8 +277,8 @@ func (w *Watcher) sendEvent(e Event) bool {
case w.Events <- e: case w.Events <- e:
return true return true
case <-w.done: case <-w.done:
return false
} }
return false
} }
// Returns true if the error was sent, or false if watcher is closed. // Returns true if the error was sent, or false if watcher is closed.
@ -180,17 +300,15 @@ func (w *Watcher) isClosed() bool {
} }
} }
// Close removes all watches and closes the events channel. // Close removes all watches and closes the Events channel.
func (w *Watcher) Close() error { func (w *Watcher) Close() error {
w.mu.Lock() w.closeMu.Lock()
if w.isClosed() { if w.isClosed() {
w.mu.Unlock() w.closeMu.Unlock()
return nil return nil
} }
// Send 'close' signal to goroutine, and set the Watcher to closed.
close(w.done) close(w.done)
w.mu.Unlock() w.closeMu.Unlock()
// Causes any blocking reads to return with an error, provided the file // Causes any blocking reads to return with an error, provided the file
// still supports deadline operations. // still supports deadline operations.
@ -207,17 +325,21 @@ func (w *Watcher) Close() error {
// Add starts monitoring the path for changes. // Add starts monitoring the path for changes.
// //
// A path can only be watched once; attempting to watch it more than once will // A path can only be watched once; watching it more than once is a no-op and will
// return an error. Paths that do not yet exist on the filesystem cannot be // not return an error. Paths that do not yet exist on the filesystem cannot be
// added. A watch will be automatically removed if the path is deleted. // watched.
// //
// A path will remain watched if it gets renamed to somewhere else on the same // A watch will be automatically removed if the watched path is deleted or
// filesystem, but the monitor will get removed if the path gets deleted and // renamed. The exception is the Windows backend, which doesn't remove the
// re-created, or if it's moved to a different filesystem. // watcher on renames.
// //
// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special // Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
// filesystems (/proc, /sys, etc.) generally don't work. // filesystems (/proc, /sys, etc.) generally don't work.
// //
// Returns [ErrClosed] if [Watcher.Close] was called.
//
// See [Watcher.AddWith] for a version that allows adding options.
//
// # Watching directories // # Watching directories
// //
// All files in a directory are monitored, including new files that are created // All files in a directory are monitored, including new files that are created
@ -227,44 +349,59 @@ func (w *Watcher) Close() error {
// # Watching files // # Watching files
// //
// Watching individual files (rather than directories) is generally not // Watching individual files (rather than directories) is generally not
// recommended as many tools update files atomically. Instead of "just" writing // recommended as many programs (especially editors) update files atomically: it
// to the file a temporary file will be written to first, and if successful the // will write to a temporary file which is then moved to to destination,
// temporary file is moved to to destination removing the original, or some // overwriting the original (or some variant thereof). The watcher on the
// variant thereof. The watcher on the original file is now lost, as it no // original file is now lost, as that no longer exists.
// longer exists.
// //
// Instead, watch the parent directory and use Event.Name to filter out files // The upshot of this is that a power failure or crash won't leave a
// you're not interested in. There is an example of this in [cmd/fsnotify/file.go]. // half-written file.
func (w *Watcher) Add(name string) error { //
name = filepath.Clean(name) // Watch the parent directory and use Event.Name to filter out files you're not
// interested in. There is an example of this in cmd/fsnotify/file.go.
func (w *Watcher) Add(name string) error { return w.AddWith(name) }
// AddWith is like [Watcher.Add], but allows adding options. When using Add()
// the defaults described below are used.
//
// Possible options are:
//
// - [WithBufferSize] sets the buffer size for the Windows backend; no-op on
// other platforms. The default is 64K (65536 bytes).
func (w *Watcher) AddWith(name string, opts ...addOpt) error {
if w.isClosed() { if w.isClosed() {
return errors.New("inotify instance already closed") return ErrClosed
} }
name = filepath.Clean(name)
_ = getOptions(opts...)
var flags uint32 = unix.IN_MOVED_TO | unix.IN_MOVED_FROM | var flags uint32 = unix.IN_MOVED_TO | unix.IN_MOVED_FROM |
unix.IN_CREATE | unix.IN_ATTRIB | unix.IN_MODIFY | unix.IN_CREATE | unix.IN_ATTRIB | unix.IN_MODIFY |
unix.IN_MOVE_SELF | unix.IN_DELETE | unix.IN_DELETE_SELF unix.IN_MOVE_SELF | unix.IN_DELETE | unix.IN_DELETE_SELF
w.mu.Lock() return w.watches.updatePath(name, func(existing *watch) (*watch, error) {
defer w.mu.Unlock() if existing != nil {
watchEntry := w.watches[name] flags |= existing.flags | unix.IN_MASK_ADD
if watchEntry != nil { }
flags |= watchEntry.flags | unix.IN_MASK_ADD
}
wd, errno := unix.InotifyAddWatch(w.fd, name, flags)
if wd == -1 {
return errno
}
if watchEntry == nil { wd, err := unix.InotifyAddWatch(w.fd, name, flags)
w.watches[name] = &watch{wd: uint32(wd), flags: flags} if wd == -1 {
w.paths[wd] = name return nil, err
} else { }
watchEntry.wd = uint32(wd)
watchEntry.flags = flags
}
return nil if existing == nil {
return &watch{
wd: uint32(wd),
path: name,
flags: flags,
}, nil
}
existing.wd = uint32(wd)
existing.flags = flags
return existing, nil
})
} }
// Remove stops monitoring the path for changes. // Remove stops monitoring the path for changes.
@ -273,32 +410,22 @@ func (w *Watcher) Add(name string) error {
// /tmp/dir and /tmp/dir/subdir then you will need to remove both. // /tmp/dir and /tmp/dir/subdir then you will need to remove both.
// //
// Removing a path that has not yet been added returns [ErrNonExistentWatch]. // Removing a path that has not yet been added returns [ErrNonExistentWatch].
//
// Returns nil if [Watcher.Close] was called.
func (w *Watcher) Remove(name string) error { func (w *Watcher) Remove(name string) error {
name = filepath.Clean(name) if w.isClosed() {
return nil
}
return w.remove(filepath.Clean(name))
}
// Fetch the watch. func (w *Watcher) remove(name string) error {
w.mu.Lock() wd, ok := w.watches.removePath(name)
defer w.mu.Unlock()
watch, ok := w.watches[name]
// Remove it from inotify.
if !ok { if !ok {
return fmt.Errorf("%w: %s", ErrNonExistentWatch, name) return fmt.Errorf("%w: %s", ErrNonExistentWatch, name)
} }
// We successfully removed the watch if InotifyRmWatch doesn't return an success, errno := unix.InotifyRmWatch(w.fd, wd)
// error, we need to clean up our internal state to ensure it matches
// inotify's kernel state.
delete(w.paths, int(watch.wd))
delete(w.watches, name)
// inotify_rm_watch will return EINVAL if the file has been deleted;
// the inotify will already have been removed.
// watches and pathes are deleted in ignoreLinux() implicitly and asynchronously
// by calling inotify_rm_watch() below. e.g. readEvents() goroutine receives IN_IGNORE
// so that EINVAL means that the wd is being rm_watch()ed or its file removed
// by another thread and we have not received IN_IGNORE event.
success, errno := unix.InotifyRmWatch(w.fd, watch.wd)
if success == -1 { if success == -1 {
// TODO: Perhaps it's not helpful to return an error here in every case; // TODO: Perhaps it's not helpful to return an error here in every case;
// The only two possible errors are: // The only two possible errors are:
@ -312,26 +439,26 @@ func (w *Watcher) Remove(name string) error {
// are watching is deleted. // are watching is deleted.
return errno return errno
} }
return nil return nil
} }
// WatchList returns all paths added with [Add] (and are not yet removed). // WatchList returns all paths explicitly added with [Watcher.Add] (and are not
// yet removed).
//
// Returns nil if [Watcher.Close] was called.
func (w *Watcher) WatchList() []string { func (w *Watcher) WatchList() []string {
w.mu.Lock() if w.isClosed() {
defer w.mu.Unlock() return nil
entries := make([]string, 0, len(w.watches))
for pathname := range w.watches {
entries = append(entries, pathname)
} }
return entries entries := make([]string, 0, w.watches.len())
} w.watches.mu.RLock()
for pathname := range w.watches.path {
entries = append(entries, pathname)
}
w.watches.mu.RUnlock()
type watch struct { return entries
wd uint32 // Watch descriptor (as returned by the inotify_add_watch() syscall)
flags uint32 // inotify flags of this watch (see inotify(7) for the list of valid flags)
} }
// readEvents reads from the inotify file descriptor, converts the // readEvents reads from the inotify file descriptor, converts the
@ -367,14 +494,11 @@ func (w *Watcher) readEvents() {
if n < unix.SizeofInotifyEvent { if n < unix.SizeofInotifyEvent {
var err error var err error
if n == 0 { if n == 0 {
// If EOF is received. This should really never happen. err = io.EOF // If EOF is received. This should really never happen.
err = io.EOF
} else if n < 0 { } else if n < 0 {
// If an error occurred while reading. err = errno // If an error occurred while reading.
err = errno
} else { } else {
// Read was too short. err = errors.New("notify: short read in readEvents()") // Read was too short.
err = errors.New("notify: short read in readEvents()")
} }
if !w.sendError(err) { if !w.sendError(err) {
return return
@ -403,18 +527,29 @@ func (w *Watcher) readEvents() {
// doesn't append the filename to the event, but we would like to always fill the // doesn't append the filename to the event, but we would like to always fill the
// the "Name" field with a valid filename. We retrieve the path of the watch from // the "Name" field with a valid filename. We retrieve the path of the watch from
// the "paths" map. // the "paths" map.
w.mu.Lock() watch := w.watches.byWd(uint32(raw.Wd))
name, ok := w.paths[int(raw.Wd)]
// IN_DELETE_SELF occurs when the file/directory being watched is removed.
// This is a sign to clean up the maps, otherwise we are no longer in sync
// with the inotify kernel state which has already deleted the watch
// automatically.
if ok && mask&unix.IN_DELETE_SELF == unix.IN_DELETE_SELF {
delete(w.paths, int(raw.Wd))
delete(w.watches, name)
}
w.mu.Unlock()
// inotify will automatically remove the watch on deletes; just need
// to clean our state here.
if watch != nil && mask&unix.IN_DELETE_SELF == unix.IN_DELETE_SELF {
w.watches.remove(watch.wd)
}
// We can't really update the state when a watched path is moved;
// only IN_MOVE_SELF is sent and not IN_MOVED_{FROM,TO}. So remove
// the watch.
if watch != nil && mask&unix.IN_MOVE_SELF == unix.IN_MOVE_SELF {
err := w.remove(watch.path)
if err != nil && !errors.Is(err, ErrNonExistentWatch) {
if !w.sendError(err) {
return
}
}
}
var name string
if watch != nil {
name = watch.path
}
if nameLen > 0 { if nameLen > 0 {
// Point "bytes" at the first byte of the filename // Point "bytes" at the first byte of the filename
bytes := (*[unix.PathMax]byte)(unsafe.Pointer(&buf[offset+unix.SizeofInotifyEvent]))[:nameLen:nameLen] bytes := (*[unix.PathMax]byte)(unsafe.Pointer(&buf[offset+unix.SizeofInotifyEvent]))[:nameLen:nameLen]

View file

@ -1,12 +1,14 @@
//go:build freebsd || openbsd || netbsd || dragonfly || darwin //go:build freebsd || openbsd || netbsd || dragonfly || darwin
// +build freebsd openbsd netbsd dragonfly darwin // +build freebsd openbsd netbsd dragonfly darwin
// Note: the documentation on the Watcher type and methods is generated from
// mkdoc.zsh
package fsnotify package fsnotify
import ( import (
"errors" "errors"
"fmt" "fmt"
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"sync" "sync"
@ -24,9 +26,9 @@ import (
// When a file is removed a Remove event won't be emitted until all file // When a file is removed a Remove event won't be emitted until all file
// descriptors are closed, and deletes will always emit a Chmod. For example: // descriptors are closed, and deletes will always emit a Chmod. For example:
// //
// fp := os.Open("file") // fp := os.Open("file")
// os.Remove("file") // Triggers Chmod // os.Remove("file") // Triggers Chmod
// fp.Close() // Triggers Remove // fp.Close() // Triggers Remove
// //
// This is the event that inotify sends, so not much can be changed about this. // This is the event that inotify sends, so not much can be changed about this.
// //
@ -40,16 +42,16 @@ import (
// //
// To increase them you can use sysctl or write the value to the /proc file: // To increase them you can use sysctl or write the value to the /proc file:
// //
// # Default values on Linux 5.18 // # Default values on Linux 5.18
// sysctl fs.inotify.max_user_watches=124983 // sysctl fs.inotify.max_user_watches=124983
// sysctl fs.inotify.max_user_instances=128 // sysctl fs.inotify.max_user_instances=128
// //
// To make the changes persist on reboot edit /etc/sysctl.conf or // To make the changes persist on reboot edit /etc/sysctl.conf or
// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check // /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
// your distro's documentation): // your distro's documentation):
// //
// fs.inotify.max_user_watches=124983 // fs.inotify.max_user_watches=124983
// fs.inotify.max_user_instances=128 // fs.inotify.max_user_instances=128
// //
// Reaching the limit will result in a "no space left on device" or "too many open // Reaching the limit will result in a "no space left on device" or "too many open
// files" error. // files" error.
@ -65,14 +67,20 @@ import (
// control the maximum number of open files, as well as /etc/login.conf on BSD // control the maximum number of open files, as well as /etc/login.conf on BSD
// systems. // systems.
// //
// # macOS notes // # Windows notes
// //
// Spotlight indexing on macOS can result in multiple events (see [#15]). A // Paths can be added as "C:\path\to\dir", but forward slashes
// temporary workaround is to add your folder(s) to the "Spotlight Privacy // ("C:/path/to/dir") will also work.
// Settings" until we have a native FSEvents implementation (see [#11]).
// //
// [#11]: https://github.com/fsnotify/fsnotify/issues/11 // When a watched directory is removed it will always send an event for the
// [#15]: https://github.com/fsnotify/fsnotify/issues/15 // directory itself, but may not send events for all files in that directory.
// Sometimes it will send events for all times, sometimes it will send no
// events, and often only for some files.
//
// The default ReadDirectoryChangesW() buffer size is 64K, which is the largest
// value that is guaranteed to work with SMB filesystems. If you have many
// events in quick succession this may not be enough, and you will have to use
// [WithBufferSize] to increase the value.
type Watcher struct { type Watcher struct {
// Events sends the filesystem change events. // Events sends the filesystem change events.
// //
@ -99,18 +107,27 @@ type Watcher struct {
// initiated by the user may show up as one or multiple // initiated by the user may show up as one or multiple
// writes, depending on when the system syncs things to // writes, depending on when the system syncs things to
// disk. For example when compiling a large Go program // disk. For example when compiling a large Go program
// you may get hundreds of Write events, so you // you may get hundreds of Write events, and you may
// probably want to wait until you've stopped receiving // want to wait until you've stopped receiving them
// them (see the dedup example in cmd/fsnotify). // (see the dedup example in cmd/fsnotify).
//
// Some systems may send Write event for directories
// when the directory content changes.
// //
// fsnotify.Chmod Attributes were changed. On Linux this is also sent // fsnotify.Chmod Attributes were changed. On Linux this is also sent
// when a file is removed (or more accurately, when a // when a file is removed (or more accurately, when a
// link to an inode is removed). On kqueue it's sent // link to an inode is removed). On kqueue it's sent
// and on kqueue when a file is truncated. On Windows // when a file is truncated. On Windows it's never
// it's never sent. // sent.
Events chan Event Events chan Event
// Errors sends any errors. // Errors sends any errors.
//
// ErrEventOverflow is used to indicate there are too many events:
//
// - inotify: There are too many queued events (fs.inotify.max_queued_events sysctl)
// - windows: The buffer size is too small; WithBufferSize() can be used to increase it.
// - kqueue, fen: Not used.
Errors chan error Errors chan error
done chan struct{} done chan struct{}
@ -133,6 +150,18 @@ type pathInfo struct {
// NewWatcher creates a new Watcher. // NewWatcher creates a new Watcher.
func NewWatcher() (*Watcher, error) { func NewWatcher() (*Watcher, error) {
return NewBufferedWatcher(0)
}
// NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events
// channel.
//
// The main use case for this is situations with a very large number of events
// where the kernel buffer size can't be increased (e.g. due to lack of
// permissions). An unbuffered Watcher will perform better for almost all use
// cases, and whenever possible you will be better off increasing the kernel
// buffers instead of adding a large userspace buffer.
func NewBufferedWatcher(sz uint) (*Watcher, error) {
kq, closepipe, err := newKqueue() kq, closepipe, err := newKqueue()
if err != nil { if err != nil {
return nil, err return nil, err
@ -147,7 +176,7 @@ func NewWatcher() (*Watcher, error) {
paths: make(map[int]pathInfo), paths: make(map[int]pathInfo),
fileExists: make(map[string]struct{}), fileExists: make(map[string]struct{}),
userWatches: make(map[string]struct{}), userWatches: make(map[string]struct{}),
Events: make(chan Event), Events: make(chan Event, sz),
Errors: make(chan error), Errors: make(chan error),
done: make(chan struct{}), done: make(chan struct{}),
} }
@ -197,8 +226,8 @@ func (w *Watcher) sendEvent(e Event) bool {
case w.Events <- e: case w.Events <- e:
return true return true
case <-w.done: case <-w.done:
return false
} }
return false
} }
// Returns true if the error was sent, or false if watcher is closed. // Returns true if the error was sent, or false if watcher is closed.
@ -207,11 +236,11 @@ func (w *Watcher) sendError(err error) bool {
case w.Errors <- err: case w.Errors <- err:
return true return true
case <-w.done: case <-w.done:
return false
} }
return false
} }
// Close removes all watches and closes the events channel. // Close removes all watches and closes the Events channel.
func (w *Watcher) Close() error { func (w *Watcher) Close() error {
w.mu.Lock() w.mu.Lock()
if w.isClosed { if w.isClosed {
@ -239,17 +268,21 @@ func (w *Watcher) Close() error {
// Add starts monitoring the path for changes. // Add starts monitoring the path for changes.
// //
// A path can only be watched once; attempting to watch it more than once will // A path can only be watched once; watching it more than once is a no-op and will
// return an error. Paths that do not yet exist on the filesystem cannot be // not return an error. Paths that do not yet exist on the filesystem cannot be
// added. A watch will be automatically removed if the path is deleted. // watched.
// //
// A path will remain watched if it gets renamed to somewhere else on the same // A watch will be automatically removed if the watched path is deleted or
// filesystem, but the monitor will get removed if the path gets deleted and // renamed. The exception is the Windows backend, which doesn't remove the
// re-created, or if it's moved to a different filesystem. // watcher on renames.
// //
// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special // Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
// filesystems (/proc, /sys, etc.) generally don't work. // filesystems (/proc, /sys, etc.) generally don't work.
// //
// Returns [ErrClosed] if [Watcher.Close] was called.
//
// See [Watcher.AddWith] for a version that allows adding options.
//
// # Watching directories // # Watching directories
// //
// All files in a directory are monitored, including new files that are created // All files in a directory are monitored, including new files that are created
@ -259,15 +292,28 @@ func (w *Watcher) Close() error {
// # Watching files // # Watching files
// //
// Watching individual files (rather than directories) is generally not // Watching individual files (rather than directories) is generally not
// recommended as many tools update files atomically. Instead of "just" writing // recommended as many programs (especially editors) update files atomically: it
// to the file a temporary file will be written to first, and if successful the // will write to a temporary file which is then moved to to destination,
// temporary file is moved to to destination removing the original, or some // overwriting the original (or some variant thereof). The watcher on the
// variant thereof. The watcher on the original file is now lost, as it no // original file is now lost, as that no longer exists.
// longer exists.
// //
// Instead, watch the parent directory and use Event.Name to filter out files // The upshot of this is that a power failure or crash won't leave a
// you're not interested in. There is an example of this in [cmd/fsnotify/file.go]. // half-written file.
func (w *Watcher) Add(name string) error { //
// Watch the parent directory and use Event.Name to filter out files you're not
// interested in. There is an example of this in cmd/fsnotify/file.go.
func (w *Watcher) Add(name string) error { return w.AddWith(name) }
// AddWith is like [Watcher.Add], but allows adding options. When using Add()
// the defaults described below are used.
//
// Possible options are:
//
// - [WithBufferSize] sets the buffer size for the Windows backend; no-op on
// other platforms. The default is 64K (65536 bytes).
func (w *Watcher) AddWith(name string, opts ...addOpt) error {
_ = getOptions(opts...)
w.mu.Lock() w.mu.Lock()
w.userWatches[name] = struct{}{} w.userWatches[name] = struct{}{}
w.mu.Unlock() w.mu.Unlock()
@ -281,9 +327,19 @@ func (w *Watcher) Add(name string) error {
// /tmp/dir and /tmp/dir/subdir then you will need to remove both. // /tmp/dir and /tmp/dir/subdir then you will need to remove both.
// //
// Removing a path that has not yet been added returns [ErrNonExistentWatch]. // Removing a path that has not yet been added returns [ErrNonExistentWatch].
//
// Returns nil if [Watcher.Close] was called.
func (w *Watcher) Remove(name string) error { func (w *Watcher) Remove(name string) error {
return w.remove(name, true)
}
func (w *Watcher) remove(name string, unwatchFiles bool) error {
name = filepath.Clean(name) name = filepath.Clean(name)
w.mu.Lock() w.mu.Lock()
if w.isClosed {
w.mu.Unlock()
return nil
}
watchfd, ok := w.watches[name] watchfd, ok := w.watches[name]
w.mu.Unlock() w.mu.Unlock()
if !ok { if !ok {
@ -315,7 +371,7 @@ func (w *Watcher) Remove(name string) error {
w.mu.Unlock() w.mu.Unlock()
// Find all watched paths that are in this directory that are not external. // Find all watched paths that are in this directory that are not external.
if isDir { if unwatchFiles && isDir {
var pathsToRemove []string var pathsToRemove []string
w.mu.Lock() w.mu.Lock()
for fd := range w.watchesByDir[name] { for fd := range w.watchesByDir[name] {
@ -326,20 +382,25 @@ func (w *Watcher) Remove(name string) error {
} }
w.mu.Unlock() w.mu.Unlock()
for _, name := range pathsToRemove { for _, name := range pathsToRemove {
// Since these are internal, not much sense in propagating error // Since these are internal, not much sense in propagating error to
// to the user, as that will just confuse them with an error about // the user, as that will just confuse them with an error about a
// a path they did not explicitly watch themselves. // path they did not explicitly watch themselves.
w.Remove(name) w.Remove(name)
} }
} }
return nil return nil
} }
// WatchList returns all paths added with [Add] (and are not yet removed). // WatchList returns all paths explicitly added with [Watcher.Add] (and are not
// yet removed).
//
// Returns nil if [Watcher.Close] was called.
func (w *Watcher) WatchList() []string { func (w *Watcher) WatchList() []string {
w.mu.Lock() w.mu.Lock()
defer w.mu.Unlock() defer w.mu.Unlock()
if w.isClosed {
return nil
}
entries := make([]string, 0, len(w.userWatches)) entries := make([]string, 0, len(w.userWatches))
for pathname := range w.userWatches { for pathname := range w.userWatches {
@ -352,18 +413,18 @@ func (w *Watcher) WatchList() []string {
// Watch all events (except NOTE_EXTEND, NOTE_LINK, NOTE_REVOKE) // Watch all events (except NOTE_EXTEND, NOTE_LINK, NOTE_REVOKE)
const noteAllEvents = unix.NOTE_DELETE | unix.NOTE_WRITE | unix.NOTE_ATTRIB | unix.NOTE_RENAME const noteAllEvents = unix.NOTE_DELETE | unix.NOTE_WRITE | unix.NOTE_ATTRIB | unix.NOTE_RENAME
// addWatch adds name to the watched file set. // addWatch adds name to the watched file set; the flags are interpreted as
// The flags are interpreted as described in kevent(2). // described in kevent(2).
// Returns the real path to the file which was added, if any, which may be different from the one passed in the case of symlinks. //
// Returns the real path to the file which was added, with symlinks resolved.
func (w *Watcher) addWatch(name string, flags uint32) (string, error) { func (w *Watcher) addWatch(name string, flags uint32) (string, error) {
var isDir bool var isDir bool
// Make ./name and name equivalent
name = filepath.Clean(name) name = filepath.Clean(name)
w.mu.Lock() w.mu.Lock()
if w.isClosed { if w.isClosed {
w.mu.Unlock() w.mu.Unlock()
return "", errors.New("kevent instance already closed") return "", ErrClosed
} }
watchfd, alreadyWatching := w.watches[name] watchfd, alreadyWatching := w.watches[name]
// We already have a watch, but we can still override flags. // We already have a watch, but we can still override flags.
@ -383,27 +444,30 @@ func (w *Watcher) addWatch(name string, flags uint32) (string, error) {
return "", nil return "", nil
} }
// Follow Symlinks // Follow Symlinks.
//
// Linux can add unresolvable symlinks to the watch list without issue,
// and Windows can't do symlinks period. To maintain consistency, we
// will act like everything is fine if the link can't be resolved.
// There will simply be no file events for broken symlinks. Hence the
// returns of nil on errors.
if fi.Mode()&os.ModeSymlink == os.ModeSymlink { if fi.Mode()&os.ModeSymlink == os.ModeSymlink {
name, err = filepath.EvalSymlinks(name) link, err := os.Readlink(name)
if err != nil { if err != nil {
// Return nil because Linux can add unresolvable symlinks to the
// watch list without problems, so maintain consistency with
// that. There will be no file events for broken symlinks.
// TODO: more specific check; returns os.PathError; ENOENT?
return "", nil return "", nil
} }
w.mu.Lock() w.mu.Lock()
_, alreadyWatching = w.watches[name] _, alreadyWatching = w.watches[link]
w.mu.Unlock() w.mu.Unlock()
if alreadyWatching { if alreadyWatching {
return name, nil // Add to watches so we don't get spurious Create events later
// on when we diff the directories.
w.watches[name] = 0
w.fileExists[name] = struct{}{}
return link, nil
} }
name = link
fi, err = os.Lstat(name) fi, err = os.Lstat(name)
if err != nil { if err != nil {
return "", nil return "", nil
@ -411,7 +475,7 @@ func (w *Watcher) addWatch(name string, flags uint32) (string, error) {
} }
// Retry on EINTR; open() can return EINTR in practice on macOS. // Retry on EINTR; open() can return EINTR in practice on macOS.
// See #354, and go issues 11180 and 39237. // See #354, and Go issues 11180 and 39237.
for { for {
watchfd, err = unix.Open(name, openMode, 0) watchfd, err = unix.Open(name, openMode, 0)
if err == nil { if err == nil {
@ -444,14 +508,13 @@ func (w *Watcher) addWatch(name string, flags uint32) (string, error) {
w.watchesByDir[parentName] = watchesByDir w.watchesByDir[parentName] = watchesByDir
} }
watchesByDir[watchfd] = struct{}{} watchesByDir[watchfd] = struct{}{}
w.paths[watchfd] = pathInfo{name: name, isDir: isDir} w.paths[watchfd] = pathInfo{name: name, isDir: isDir}
w.mu.Unlock() w.mu.Unlock()
} }
if isDir { if isDir {
// Watch the directory if it has not been watched before, // Watch the directory if it has not been watched before, or if it was
// or if it was watched before, but perhaps only a NOTE_DELETE (watchDirectoryFiles) // watched before, but perhaps only a NOTE_DELETE (watchDirectoryFiles)
w.mu.Lock() w.mu.Lock()
watchDir := (flags&unix.NOTE_WRITE) == unix.NOTE_WRITE && watchDir := (flags&unix.NOTE_WRITE) == unix.NOTE_WRITE &&
@ -473,13 +536,10 @@ func (w *Watcher) addWatch(name string, flags uint32) (string, error) {
// Event values that it sends down the Events channel. // Event values that it sends down the Events channel.
func (w *Watcher) readEvents() { func (w *Watcher) readEvents() {
defer func() { defer func() {
err := unix.Close(w.kq)
if err != nil {
w.Errors <- err
}
unix.Close(w.closepipe[0])
close(w.Events) close(w.Events)
close(w.Errors) close(w.Errors)
_ = unix.Close(w.kq)
unix.Close(w.closepipe[0])
}() }()
eventBuffer := make([]unix.Kevent_t, 10) eventBuffer := make([]unix.Kevent_t, 10)
@ -513,18 +573,8 @@ func (w *Watcher) readEvents() {
event := w.newEvent(path.name, mask) event := w.newEvent(path.name, mask)
if path.isDir && !event.Has(Remove) {
// Double check to make sure the directory exists. This can
// happen when we do a rm -fr on a recursively watched folders
// and we receive a modification event first but the folder has
// been deleted and later receive the delete event.
if _, err := os.Lstat(event.Name); os.IsNotExist(err) {
event.Op |= Remove
}
}
if event.Has(Rename) || event.Has(Remove) { if event.Has(Rename) || event.Has(Remove) {
w.Remove(event.Name) w.remove(event.Name, false)
w.mu.Lock() w.mu.Lock()
delete(w.fileExists, event.Name) delete(w.fileExists, event.Name)
w.mu.Unlock() w.mu.Unlock()
@ -540,26 +590,30 @@ func (w *Watcher) readEvents() {
} }
if event.Has(Remove) { if event.Has(Remove) {
// Look for a file that may have overwritten this. // Look for a file that may have overwritten this; for example,
// For example, mv f1 f2 will delete f2, then create f2. // mv f1 f2 will delete f2, then create f2.
if path.isDir { if path.isDir {
fileDir := filepath.Clean(event.Name) fileDir := filepath.Clean(event.Name)
w.mu.Lock() w.mu.Lock()
_, found := w.watches[fileDir] _, found := w.watches[fileDir]
w.mu.Unlock() w.mu.Unlock()
if found { if found {
// make sure the directory exists before we watch for changes. When we err := w.sendDirectoryChangeEvents(fileDir)
// do a recursive watch and perform rm -fr, the parent directory might if err != nil {
// have gone missing, ignore the missing directory and let the if !w.sendError(err) {
// upcoming delete event remove the watch from the parent directory. closed = true
if _, err := os.Lstat(fileDir); err == nil { }
w.sendDirectoryChangeEvents(fileDir)
} }
} }
} else { } else {
filePath := filepath.Clean(event.Name) filePath := filepath.Clean(event.Name)
if fileInfo, err := os.Lstat(filePath); err == nil { if fi, err := os.Lstat(filePath); err == nil {
w.sendFileCreatedEventIfNew(filePath, fileInfo) err := w.sendFileCreatedEventIfNew(filePath, fi)
if err != nil {
if !w.sendError(err) {
closed = true
}
}
} }
} }
} }
@ -582,21 +636,31 @@ func (w *Watcher) newEvent(name string, mask uint32) Event {
if mask&unix.NOTE_ATTRIB == unix.NOTE_ATTRIB { if mask&unix.NOTE_ATTRIB == unix.NOTE_ATTRIB {
e.Op |= Chmod e.Op |= Chmod
} }
// No point sending a write and delete event at the same time: if it's gone,
// then it's gone.
if e.Op.Has(Write) && e.Op.Has(Remove) {
e.Op &^= Write
}
return e return e
} }
// watchDirectoryFiles to mimic inotify when adding a watch on a directory // watchDirectoryFiles to mimic inotify when adding a watch on a directory
func (w *Watcher) watchDirectoryFiles(dirPath string) error { func (w *Watcher) watchDirectoryFiles(dirPath string) error {
// Get all files // Get all files
files, err := ioutil.ReadDir(dirPath) files, err := os.ReadDir(dirPath)
if err != nil { if err != nil {
return err return err
} }
for _, fileInfo := range files { for _, f := range files {
path := filepath.Join(dirPath, fileInfo.Name()) path := filepath.Join(dirPath, f.Name())
cleanPath, err := w.internalWatch(path, fileInfo) fi, err := f.Info()
if err != nil {
return fmt.Errorf("%q: %w", path, err)
}
cleanPath, err := w.internalWatch(path, fi)
if err != nil { if err != nil {
// No permission to read the file; that's not a problem: just skip. // No permission to read the file; that's not a problem: just skip.
// But do add it to w.fileExists to prevent it from being picked up // But do add it to w.fileExists to prevent it from being picked up
@ -606,7 +670,7 @@ func (w *Watcher) watchDirectoryFiles(dirPath string) error {
case errors.Is(err, unix.EACCES) || errors.Is(err, unix.EPERM): case errors.Is(err, unix.EACCES) || errors.Is(err, unix.EPERM):
cleanPath = filepath.Clean(path) cleanPath = filepath.Clean(path)
default: default:
return fmt.Errorf("%q: %w", filepath.Join(dirPath, fileInfo.Name()), err) return fmt.Errorf("%q: %w", path, err)
} }
} }
@ -622,26 +686,37 @@ func (w *Watcher) watchDirectoryFiles(dirPath string) error {
// //
// This functionality is to have the BSD watcher match the inotify, which sends // This functionality is to have the BSD watcher match the inotify, which sends
// a create event for files created in a watched directory. // a create event for files created in a watched directory.
func (w *Watcher) sendDirectoryChangeEvents(dir string) { func (w *Watcher) sendDirectoryChangeEvents(dir string) error {
// Get all files files, err := os.ReadDir(dir)
files, err := ioutil.ReadDir(dir)
if err != nil { if err != nil {
if !w.sendError(fmt.Errorf("fsnotify.sendDirectoryChangeEvents: %w", err)) { // Directory no longer exists: we can ignore this safely. kqueue will
return // still give us the correct events.
if errors.Is(err, os.ErrNotExist) {
return nil
} }
return fmt.Errorf("fsnotify.sendDirectoryChangeEvents: %w", err)
} }
// Search for new files for _, f := range files {
for _, fi := range files { fi, err := f.Info()
err := w.sendFileCreatedEventIfNew(filepath.Join(dir, fi.Name()), fi)
if err != nil { if err != nil {
return return fmt.Errorf("fsnotify.sendDirectoryChangeEvents: %w", err)
}
err = w.sendFileCreatedEventIfNew(filepath.Join(dir, fi.Name()), fi)
if err != nil {
// Don't need to send an error if this file isn't readable.
if errors.Is(err, unix.EACCES) || errors.Is(err, unix.EPERM) {
return nil
}
return fmt.Errorf("fsnotify.sendDirectoryChangeEvents: %w", err)
} }
} }
return nil
} }
// sendFileCreatedEvent sends a create event if the file isn't already being tracked. // sendFileCreatedEvent sends a create event if the file isn't already being tracked.
func (w *Watcher) sendFileCreatedEventIfNew(filePath string, fileInfo os.FileInfo) (err error) { func (w *Watcher) sendFileCreatedEventIfNew(filePath string, fi os.FileInfo) (err error) {
w.mu.Lock() w.mu.Lock()
_, doesExist := w.fileExists[filePath] _, doesExist := w.fileExists[filePath]
w.mu.Unlock() w.mu.Unlock()
@ -652,7 +727,7 @@ func (w *Watcher) sendFileCreatedEventIfNew(filePath string, fileInfo os.FileInf
} }
// like watchDirectoryFiles (but without doing another ReadDir) // like watchDirectoryFiles (but without doing another ReadDir)
filePath, err = w.internalWatch(filePath, fileInfo) filePath, err = w.internalWatch(filePath, fi)
if err != nil { if err != nil {
return err return err
} }
@ -664,10 +739,10 @@ func (w *Watcher) sendFileCreatedEventIfNew(filePath string, fileInfo os.FileInf
return nil return nil
} }
func (w *Watcher) internalWatch(name string, fileInfo os.FileInfo) (string, error) { func (w *Watcher) internalWatch(name string, fi os.FileInfo) (string, error) {
if fileInfo.IsDir() { if fi.IsDir() {
// mimic Linux providing delete events for subdirectories // mimic Linux providing delete events for subdirectories, but preserve
// but preserve the flags used if currently watching subdirectory // the flags used if currently watching subdirectory
w.mu.Lock() w.mu.Lock()
flags := w.dirFlags[name] flags := w.dirFlags[name]
w.mu.Unlock() w.mu.Unlock()

View file

@ -1,39 +1,169 @@
//go:build !darwin && !dragonfly && !freebsd && !openbsd && !linux && !netbsd && !solaris && !windows //go:build appengine || (!darwin && !dragonfly && !freebsd && !openbsd && !linux && !netbsd && !solaris && !windows)
// +build !darwin,!dragonfly,!freebsd,!openbsd,!linux,!netbsd,!solaris,!windows // +build appengine !darwin,!dragonfly,!freebsd,!openbsd,!linux,!netbsd,!solaris,!windows
// Note: the documentation on the Watcher type and methods is generated from
// mkdoc.zsh
package fsnotify package fsnotify
import ( import "errors"
"fmt"
"runtime"
)
// Watcher watches a set of files, delivering events to a channel. // Watcher watches a set of paths, delivering events on a channel.
type Watcher struct{} //
// A watcher should not be copied (e.g. pass it by pointer, rather than by
// value).
//
// # Linux notes
//
// When a file is removed a Remove event won't be emitted until all file
// descriptors are closed, and deletes will always emit a Chmod. For example:
//
// fp := os.Open("file")
// os.Remove("file") // Triggers Chmod
// fp.Close() // Triggers Remove
//
// This is the event that inotify sends, so not much can be changed about this.
//
// The fs.inotify.max_user_watches sysctl variable specifies the upper limit
// for the number of watches per user, and fs.inotify.max_user_instances
// specifies the maximum number of inotify instances per user. Every Watcher you
// create is an "instance", and every path you add is a "watch".
//
// These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and
// /proc/sys/fs/inotify/max_user_instances
//
// To increase them you can use sysctl or write the value to the /proc file:
//
// # Default values on Linux 5.18
// sysctl fs.inotify.max_user_watches=124983
// sysctl fs.inotify.max_user_instances=128
//
// To make the changes persist on reboot edit /etc/sysctl.conf or
// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
// your distro's documentation):
//
// fs.inotify.max_user_watches=124983
// fs.inotify.max_user_instances=128
//
// Reaching the limit will result in a "no space left on device" or "too many open
// files" error.
//
// # kqueue notes (macOS, BSD)
//
// kqueue requires opening a file descriptor for every file that's being watched;
// so if you're watching a directory with five files then that's six file
// descriptors. You will run in to your system's "max open files" limit faster on
// these platforms.
//
// The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to
// control the maximum number of open files, as well as /etc/login.conf on BSD
// systems.
//
// # Windows notes
//
// Paths can be added as "C:\path\to\dir", but forward slashes
// ("C:/path/to/dir") will also work.
//
// When a watched directory is removed it will always send an event for the
// directory itself, but may not send events for all files in that directory.
// Sometimes it will send events for all times, sometimes it will send no
// events, and often only for some files.
//
// The default ReadDirectoryChangesW() buffer size is 64K, which is the largest
// value that is guaranteed to work with SMB filesystems. If you have many
// events in quick succession this may not be enough, and you will have to use
// [WithBufferSize] to increase the value.
type Watcher struct {
// Events sends the filesystem change events.
//
// fsnotify can send the following events; a "path" here can refer to a
// file, directory, symbolic link, or special file like a FIFO.
//
// fsnotify.Create A new path was created; this may be followed by one
// or more Write events if data also gets written to a
// file.
//
// fsnotify.Remove A path was removed.
//
// fsnotify.Rename A path was renamed. A rename is always sent with the
// old path as Event.Name, and a Create event will be
// sent with the new name. Renames are only sent for
// paths that are currently watched; e.g. moving an
// unmonitored file into a monitored directory will
// show up as just a Create. Similarly, renaming a file
// to outside a monitored directory will show up as
// only a Rename.
//
// fsnotify.Write A file or named pipe was written to. A Truncate will
// also trigger a Write. A single "write action"
// initiated by the user may show up as one or multiple
// writes, depending on when the system syncs things to
// disk. For example when compiling a large Go program
// you may get hundreds of Write events, and you may
// want to wait until you've stopped receiving them
// (see the dedup example in cmd/fsnotify).
//
// Some systems may send Write event for directories
// when the directory content changes.
//
// fsnotify.Chmod Attributes were changed. On Linux this is also sent
// when a file is removed (or more accurately, when a
// link to an inode is removed). On kqueue it's sent
// when a file is truncated. On Windows it's never
// sent.
Events chan Event
// Errors sends any errors.
//
// ErrEventOverflow is used to indicate there are too many events:
//
// - inotify: There are too many queued events (fs.inotify.max_queued_events sysctl)
// - windows: The buffer size is too small; WithBufferSize() can be used to increase it.
// - kqueue, fen: Not used.
Errors chan error
}
// NewWatcher creates a new Watcher. // NewWatcher creates a new Watcher.
func NewWatcher() (*Watcher, error) { func NewWatcher() (*Watcher, error) {
return nil, fmt.Errorf("fsnotify not supported on %s", runtime.GOOS) return nil, errors.New("fsnotify not supported on the current platform")
} }
// Close removes all watches and closes the events channel. // NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events
func (w *Watcher) Close() error { // channel.
return nil //
} // The main use case for this is situations with a very large number of events
// where the kernel buffer size can't be increased (e.g. due to lack of
// permissions). An unbuffered Watcher will perform better for almost all use
// cases, and whenever possible you will be better off increasing the kernel
// buffers instead of adding a large userspace buffer.
func NewBufferedWatcher(sz uint) (*Watcher, error) { return NewWatcher() }
// Close removes all watches and closes the Events channel.
func (w *Watcher) Close() error { return nil }
// WatchList returns all paths explicitly added with [Watcher.Add] (and are not
// yet removed).
//
// Returns nil if [Watcher.Close] was called.
func (w *Watcher) WatchList() []string { return nil }
// Add starts monitoring the path for changes. // Add starts monitoring the path for changes.
// //
// A path can only be watched once; attempting to watch it more than once will // A path can only be watched once; watching it more than once is a no-op and will
// return an error. Paths that do not yet exist on the filesystem cannot be // not return an error. Paths that do not yet exist on the filesystem cannot be
// added. A watch will be automatically removed if the path is deleted. // watched.
// //
// A path will remain watched if it gets renamed to somewhere else on the same // A watch will be automatically removed if the watched path is deleted or
// filesystem, but the monitor will get removed if the path gets deleted and // renamed. The exception is the Windows backend, which doesn't remove the
// re-created, or if it's moved to a different filesystem. // watcher on renames.
// //
// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special // Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
// filesystems (/proc, /sys, etc.) generally don't work. // filesystems (/proc, /sys, etc.) generally don't work.
// //
// Returns [ErrClosed] if [Watcher.Close] was called.
//
// See [Watcher.AddWith] for a version that allows adding options.
//
// # Watching directories // # Watching directories
// //
// All files in a directory are monitored, including new files that are created // All files in a directory are monitored, including new files that are created
@ -43,17 +173,26 @@ func (w *Watcher) Close() error {
// # Watching files // # Watching files
// //
// Watching individual files (rather than directories) is generally not // Watching individual files (rather than directories) is generally not
// recommended as many tools update files atomically. Instead of "just" writing // recommended as many programs (especially editors) update files atomically: it
// to the file a temporary file will be written to first, and if successful the // will write to a temporary file which is then moved to to destination,
// temporary file is moved to to destination removing the original, or some // overwriting the original (or some variant thereof). The watcher on the
// variant thereof. The watcher on the original file is now lost, as it no // original file is now lost, as that no longer exists.
// longer exists.
// //
// Instead, watch the parent directory and use Event.Name to filter out files // The upshot of this is that a power failure or crash won't leave a
// you're not interested in. There is an example of this in [cmd/fsnotify/file.go]. // half-written file.
func (w *Watcher) Add(name string) error { //
return nil // Watch the parent directory and use Event.Name to filter out files you're not
} // interested in. There is an example of this in cmd/fsnotify/file.go.
func (w *Watcher) Add(name string) error { return nil }
// AddWith is like [Watcher.Add], but allows adding options. When using Add()
// the defaults described below are used.
//
// Possible options are:
//
// - [WithBufferSize] sets the buffer size for the Windows backend; no-op on
// other platforms. The default is 64K (65536 bytes).
func (w *Watcher) AddWith(name string, opts ...addOpt) error { return nil }
// Remove stops monitoring the path for changes. // Remove stops monitoring the path for changes.
// //
@ -61,6 +200,6 @@ func (w *Watcher) Add(name string) error {
// /tmp/dir and /tmp/dir/subdir then you will need to remove both. // /tmp/dir and /tmp/dir/subdir then you will need to remove both.
// //
// Removing a path that has not yet been added returns [ErrNonExistentWatch]. // Removing a path that has not yet been added returns [ErrNonExistentWatch].
func (w *Watcher) Remove(name string) error { //
return nil // Returns nil if [Watcher.Close] was called.
} func (w *Watcher) Remove(name string) error { return nil }

View file

@ -1,6 +1,13 @@
//go:build windows //go:build windows
// +build windows // +build windows
// Windows backend based on ReadDirectoryChangesW()
//
// https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-readdirectorychangesw
//
// Note: the documentation on the Watcher type and methods is generated from
// mkdoc.zsh
package fsnotify package fsnotify
import ( import (
@ -27,9 +34,9 @@ import (
// When a file is removed a Remove event won't be emitted until all file // When a file is removed a Remove event won't be emitted until all file
// descriptors are closed, and deletes will always emit a Chmod. For example: // descriptors are closed, and deletes will always emit a Chmod. For example:
// //
// fp := os.Open("file") // fp := os.Open("file")
// os.Remove("file") // Triggers Chmod // os.Remove("file") // Triggers Chmod
// fp.Close() // Triggers Remove // fp.Close() // Triggers Remove
// //
// This is the event that inotify sends, so not much can be changed about this. // This is the event that inotify sends, so not much can be changed about this.
// //
@ -43,16 +50,16 @@ import (
// //
// To increase them you can use sysctl or write the value to the /proc file: // To increase them you can use sysctl or write the value to the /proc file:
// //
// # Default values on Linux 5.18 // # Default values on Linux 5.18
// sysctl fs.inotify.max_user_watches=124983 // sysctl fs.inotify.max_user_watches=124983
// sysctl fs.inotify.max_user_instances=128 // sysctl fs.inotify.max_user_instances=128
// //
// To make the changes persist on reboot edit /etc/sysctl.conf or // To make the changes persist on reboot edit /etc/sysctl.conf or
// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check // /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
// your distro's documentation): // your distro's documentation):
// //
// fs.inotify.max_user_watches=124983 // fs.inotify.max_user_watches=124983
// fs.inotify.max_user_instances=128 // fs.inotify.max_user_instances=128
// //
// Reaching the limit will result in a "no space left on device" or "too many open // Reaching the limit will result in a "no space left on device" or "too many open
// files" error. // files" error.
@ -68,14 +75,20 @@ import (
// control the maximum number of open files, as well as /etc/login.conf on BSD // control the maximum number of open files, as well as /etc/login.conf on BSD
// systems. // systems.
// //
// # macOS notes // # Windows notes
// //
// Spotlight indexing on macOS can result in multiple events (see [#15]). A // Paths can be added as "C:\path\to\dir", but forward slashes
// temporary workaround is to add your folder(s) to the "Spotlight Privacy // ("C:/path/to/dir") will also work.
// Settings" until we have a native FSEvents implementation (see [#11]).
// //
// [#11]: https://github.com/fsnotify/fsnotify/issues/11 // When a watched directory is removed it will always send an event for the
// [#15]: https://github.com/fsnotify/fsnotify/issues/15 // directory itself, but may not send events for all files in that directory.
// Sometimes it will send events for all times, sometimes it will send no
// events, and often only for some files.
//
// The default ReadDirectoryChangesW() buffer size is 64K, which is the largest
// value that is guaranteed to work with SMB filesystems. If you have many
// events in quick succession this may not be enough, and you will have to use
// [WithBufferSize] to increase the value.
type Watcher struct { type Watcher struct {
// Events sends the filesystem change events. // Events sends the filesystem change events.
// //
@ -102,31 +115,52 @@ type Watcher struct {
// initiated by the user may show up as one or multiple // initiated by the user may show up as one or multiple
// writes, depending on when the system syncs things to // writes, depending on when the system syncs things to
// disk. For example when compiling a large Go program // disk. For example when compiling a large Go program
// you may get hundreds of Write events, so you // you may get hundreds of Write events, and you may
// probably want to wait until you've stopped receiving // want to wait until you've stopped receiving them
// them (see the dedup example in cmd/fsnotify). // (see the dedup example in cmd/fsnotify).
//
// Some systems may send Write event for directories
// when the directory content changes.
// //
// fsnotify.Chmod Attributes were changed. On Linux this is also sent // fsnotify.Chmod Attributes were changed. On Linux this is also sent
// when a file is removed (or more accurately, when a // when a file is removed (or more accurately, when a
// link to an inode is removed). On kqueue it's sent // link to an inode is removed). On kqueue it's sent
// and on kqueue when a file is truncated. On Windows // when a file is truncated. On Windows it's never
// it's never sent. // sent.
Events chan Event Events chan Event
// Errors sends any errors. // Errors sends any errors.
//
// ErrEventOverflow is used to indicate there are too many events:
//
// - inotify: There are too many queued events (fs.inotify.max_queued_events sysctl)
// - windows: The buffer size is too small; WithBufferSize() can be used to increase it.
// - kqueue, fen: Not used.
Errors chan error Errors chan error
port windows.Handle // Handle to completion port port windows.Handle // Handle to completion port
input chan *input // Inputs to the reader are sent on this channel input chan *input // Inputs to the reader are sent on this channel
quit chan chan<- error quit chan chan<- error
mu sync.Mutex // Protects access to watches, isClosed mu sync.Mutex // Protects access to watches, closed
watches watchMap // Map of watches (key: i-number) watches watchMap // Map of watches (key: i-number)
isClosed bool // Set to true when Close() is first called closed bool // Set to true when Close() is first called
} }
// NewWatcher creates a new Watcher. // NewWatcher creates a new Watcher.
func NewWatcher() (*Watcher, error) { func NewWatcher() (*Watcher, error) {
return NewBufferedWatcher(50)
}
// NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events
// channel.
//
// The main use case for this is situations with a very large number of events
// where the kernel buffer size can't be increased (e.g. due to lack of
// permissions). An unbuffered Watcher will perform better for almost all use
// cases, and whenever possible you will be better off increasing the kernel
// buffers instead of adding a large userspace buffer.
func NewBufferedWatcher(sz uint) (*Watcher, error) {
port, err := windows.CreateIoCompletionPort(windows.InvalidHandle, 0, 0, 0) port, err := windows.CreateIoCompletionPort(windows.InvalidHandle, 0, 0, 0)
if err != nil { if err != nil {
return nil, os.NewSyscallError("CreateIoCompletionPort", err) return nil, os.NewSyscallError("CreateIoCompletionPort", err)
@ -135,7 +169,7 @@ func NewWatcher() (*Watcher, error) {
port: port, port: port,
watches: make(watchMap), watches: make(watchMap),
input: make(chan *input, 1), input: make(chan *input, 1),
Events: make(chan Event, 50), Events: make(chan Event, sz),
Errors: make(chan error), Errors: make(chan error),
quit: make(chan chan<- error, 1), quit: make(chan chan<- error, 1),
} }
@ -143,6 +177,12 @@ func NewWatcher() (*Watcher, error) {
return w, nil return w, nil
} }
func (w *Watcher) isClosed() bool {
w.mu.Lock()
defer w.mu.Unlock()
return w.closed
}
func (w *Watcher) sendEvent(name string, mask uint64) bool { func (w *Watcher) sendEvent(name string, mask uint64) bool {
if mask == 0 { if mask == 0 {
return false return false
@ -167,14 +207,14 @@ func (w *Watcher) sendError(err error) bool {
return false return false
} }
// Close removes all watches and closes the events channel. // Close removes all watches and closes the Events channel.
func (w *Watcher) Close() error { func (w *Watcher) Close() error {
w.mu.Lock() if w.isClosed() {
if w.isClosed {
w.mu.Unlock()
return nil return nil
} }
w.isClosed = true
w.mu.Lock()
w.closed = true
w.mu.Unlock() w.mu.Unlock()
// Send "quit" message to the reader goroutine // Send "quit" message to the reader goroutine
@ -188,17 +228,21 @@ func (w *Watcher) Close() error {
// Add starts monitoring the path for changes. // Add starts monitoring the path for changes.
// //
// A path can only be watched once; attempting to watch it more than once will // A path can only be watched once; watching it more than once is a no-op and will
// return an error. Paths that do not yet exist on the filesystem cannot be // not return an error. Paths that do not yet exist on the filesystem cannot be
// added. A watch will be automatically removed if the path is deleted. // watched.
// //
// A path will remain watched if it gets renamed to somewhere else on the same // A watch will be automatically removed if the watched path is deleted or
// filesystem, but the monitor will get removed if the path gets deleted and // renamed. The exception is the Windows backend, which doesn't remove the
// re-created, or if it's moved to a different filesystem. // watcher on renames.
// //
// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special // Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
// filesystems (/proc, /sys, etc.) generally don't work. // filesystems (/proc, /sys, etc.) generally don't work.
// //
// Returns [ErrClosed] if [Watcher.Close] was called.
//
// See [Watcher.AddWith] for a version that allows adding options.
//
// # Watching directories // # Watching directories
// //
// All files in a directory are monitored, including new files that are created // All files in a directory are monitored, including new files that are created
@ -208,27 +252,41 @@ func (w *Watcher) Close() error {
// # Watching files // # Watching files
// //
// Watching individual files (rather than directories) is generally not // Watching individual files (rather than directories) is generally not
// recommended as many tools update files atomically. Instead of "just" writing // recommended as many programs (especially editors) update files atomically: it
// to the file a temporary file will be written to first, and if successful the // will write to a temporary file which is then moved to to destination,
// temporary file is moved to to destination removing the original, or some // overwriting the original (or some variant thereof). The watcher on the
// variant thereof. The watcher on the original file is now lost, as it no // original file is now lost, as that no longer exists.
// longer exists.
// //
// Instead, watch the parent directory and use Event.Name to filter out files // The upshot of this is that a power failure or crash won't leave a
// you're not interested in. There is an example of this in [cmd/fsnotify/file.go]. // half-written file.
func (w *Watcher) Add(name string) error { //
w.mu.Lock() // Watch the parent directory and use Event.Name to filter out files you're not
if w.isClosed { // interested in. There is an example of this in cmd/fsnotify/file.go.
w.mu.Unlock() func (w *Watcher) Add(name string) error { return w.AddWith(name) }
return errors.New("watcher already closed")
// AddWith is like [Watcher.Add], but allows adding options. When using Add()
// the defaults described below are used.
//
// Possible options are:
//
// - [WithBufferSize] sets the buffer size for the Windows backend; no-op on
// other platforms. The default is 64K (65536 bytes).
func (w *Watcher) AddWith(name string, opts ...addOpt) error {
if w.isClosed() {
return ErrClosed
}
with := getOptions(opts...)
if with.bufsize < 4096 {
return fmt.Errorf("fsnotify.WithBufferSize: buffer size cannot be smaller than 4096 bytes")
} }
w.mu.Unlock()
in := &input{ in := &input{
op: opAddWatch, op: opAddWatch,
path: filepath.Clean(name), path: filepath.Clean(name),
flags: sysFSALLEVENTS, flags: sysFSALLEVENTS,
reply: make(chan error), reply: make(chan error),
bufsize: with.bufsize,
} }
w.input <- in w.input <- in
if err := w.wakeupReader(); err != nil { if err := w.wakeupReader(); err != nil {
@ -243,7 +301,13 @@ func (w *Watcher) Add(name string) error {
// /tmp/dir and /tmp/dir/subdir then you will need to remove both. // /tmp/dir and /tmp/dir/subdir then you will need to remove both.
// //
// Removing a path that has not yet been added returns [ErrNonExistentWatch]. // Removing a path that has not yet been added returns [ErrNonExistentWatch].
//
// Returns nil if [Watcher.Close] was called.
func (w *Watcher) Remove(name string) error { func (w *Watcher) Remove(name string) error {
if w.isClosed() {
return nil
}
in := &input{ in := &input{
op: opRemoveWatch, op: opRemoveWatch,
path: filepath.Clean(name), path: filepath.Clean(name),
@ -256,8 +320,15 @@ func (w *Watcher) Remove(name string) error {
return <-in.reply return <-in.reply
} }
// WatchList returns all paths added with [Add] (and are not yet removed). // WatchList returns all paths explicitly added with [Watcher.Add] (and are not
// yet removed).
//
// Returns nil if [Watcher.Close] was called.
func (w *Watcher) WatchList() []string { func (w *Watcher) WatchList() []string {
if w.isClosed() {
return nil
}
w.mu.Lock() w.mu.Lock()
defer w.mu.Unlock() defer w.mu.Unlock()
@ -279,7 +350,6 @@ func (w *Watcher) WatchList() []string {
// This should all be removed at some point, and just use windows.FILE_NOTIFY_* // This should all be removed at some point, and just use windows.FILE_NOTIFY_*
const ( const (
sysFSALLEVENTS = 0xfff sysFSALLEVENTS = 0xfff
sysFSATTRIB = 0x4
sysFSCREATE = 0x100 sysFSCREATE = 0x100
sysFSDELETE = 0x200 sysFSDELETE = 0x200
sysFSDELETESELF = 0x400 sysFSDELETESELF = 0x400
@ -305,9 +375,6 @@ func (w *Watcher) newEvent(name string, mask uint32) Event {
if mask&sysFSMOVE == sysFSMOVE || mask&sysFSMOVESELF == sysFSMOVESELF || mask&sysFSMOVEDFROM == sysFSMOVEDFROM { if mask&sysFSMOVE == sysFSMOVE || mask&sysFSMOVESELF == sysFSMOVESELF || mask&sysFSMOVEDFROM == sysFSMOVEDFROM {
e.Op |= Rename e.Op |= Rename
} }
if mask&sysFSATTRIB == sysFSATTRIB {
e.Op |= Chmod
}
return e return e
} }
@ -321,10 +388,11 @@ const (
) )
type input struct { type input struct {
op int op int
path string path string
flags uint32 flags uint32
reply chan error bufsize int
reply chan error
} }
type inode struct { type inode struct {
@ -334,13 +402,14 @@ type inode struct {
} }
type watch struct { type watch struct {
ov windows.Overlapped ov windows.Overlapped
ino *inode // i-number ino *inode // i-number
path string // Directory path recurse bool // Recursive watch?
mask uint64 // Directory itself is being watched with these notify flags path string // Directory path
names map[string]uint64 // Map of names being watched and their notify flags mask uint64 // Directory itself is being watched with these notify flags
rename string // Remembers the old name while renaming a file names map[string]uint64 // Map of names being watched and their notify flags
buf [65536]byte // 64K buffer rename string // Remembers the old name while renaming a file
buf []byte // buffer, allocated later
} }
type ( type (
@ -413,7 +482,10 @@ func (m watchMap) set(ino *inode, watch *watch) {
} }
// Must run within the I/O thread. // Must run within the I/O thread.
func (w *Watcher) addWatch(pathname string, flags uint64) error { func (w *Watcher) addWatch(pathname string, flags uint64, bufsize int) error {
//pathname, recurse := recursivePath(pathname)
recurse := false
dir, err := w.getDir(pathname) dir, err := w.getDir(pathname)
if err != nil { if err != nil {
return err return err
@ -433,9 +505,11 @@ func (w *Watcher) addWatch(pathname string, flags uint64) error {
return os.NewSyscallError("CreateIoCompletionPort", err) return os.NewSyscallError("CreateIoCompletionPort", err)
} }
watchEntry = &watch{ watchEntry = &watch{
ino: ino, ino: ino,
path: dir, path: dir,
names: make(map[string]uint64), names: make(map[string]uint64),
recurse: recurse,
buf: make([]byte, bufsize),
} }
w.mu.Lock() w.mu.Lock()
w.watches.set(ino, watchEntry) w.watches.set(ino, watchEntry)
@ -465,6 +539,8 @@ func (w *Watcher) addWatch(pathname string, flags uint64) error {
// Must run within the I/O thread. // Must run within the I/O thread.
func (w *Watcher) remWatch(pathname string) error { func (w *Watcher) remWatch(pathname string) error {
pathname, recurse := recursivePath(pathname)
dir, err := w.getDir(pathname) dir, err := w.getDir(pathname)
if err != nil { if err != nil {
return err return err
@ -478,6 +554,10 @@ func (w *Watcher) remWatch(pathname string) error {
watch := w.watches.get(ino) watch := w.watches.get(ino)
w.mu.Unlock() w.mu.Unlock()
if recurse && !watch.recurse {
return fmt.Errorf("can't use \\... with non-recursive watch %q", pathname)
}
err = windows.CloseHandle(ino.handle) err = windows.CloseHandle(ino.handle)
if err != nil { if err != nil {
w.sendError(os.NewSyscallError("CloseHandle", err)) w.sendError(os.NewSyscallError("CloseHandle", err))
@ -535,8 +615,11 @@ func (w *Watcher) startRead(watch *watch) error {
return nil return nil
} }
rdErr := windows.ReadDirectoryChanges(watch.ino.handle, &watch.buf[0], // We need to pass the array, rather than the slice.
uint32(unsafe.Sizeof(watch.buf)), false, mask, nil, &watch.ov, 0) hdr := (*reflect.SliceHeader)(unsafe.Pointer(&watch.buf))
rdErr := windows.ReadDirectoryChanges(watch.ino.handle,
(*byte)(unsafe.Pointer(hdr.Data)), uint32(hdr.Len),
watch.recurse, mask, nil, &watch.ov, 0)
if rdErr != nil { if rdErr != nil {
err := os.NewSyscallError("ReadDirectoryChanges", rdErr) err := os.NewSyscallError("ReadDirectoryChanges", rdErr)
if rdErr == windows.ERROR_ACCESS_DENIED && watch.mask&provisional == 0 { if rdErr == windows.ERROR_ACCESS_DENIED && watch.mask&provisional == 0 {
@ -563,9 +646,8 @@ func (w *Watcher) readEvents() {
runtime.LockOSThread() runtime.LockOSThread()
for { for {
// This error is handled after the watch == nil check below.
qErr := windows.GetQueuedCompletionStatus(w.port, &n, &key, &ov, windows.INFINITE) qErr := windows.GetQueuedCompletionStatus(w.port, &n, &key, &ov, windows.INFINITE)
// This error is handled after the watch == nil check below. NOTE: this
// seems odd, note sure if it's correct.
watch := (*watch)(unsafe.Pointer(ov)) watch := (*watch)(unsafe.Pointer(ov))
if watch == nil { if watch == nil {
@ -595,7 +677,7 @@ func (w *Watcher) readEvents() {
case in := <-w.input: case in := <-w.input:
switch in.op { switch in.op {
case opAddWatch: case opAddWatch:
in.reply <- w.addWatch(in.path, uint64(in.flags)) in.reply <- w.addWatch(in.path, uint64(in.flags), in.bufsize)
case opRemoveWatch: case opRemoveWatch:
in.reply <- w.remWatch(in.path) in.reply <- w.remWatch(in.path)
} }
@ -605,6 +687,8 @@ func (w *Watcher) readEvents() {
} }
switch qErr { switch qErr {
case nil:
// No error
case windows.ERROR_MORE_DATA: case windows.ERROR_MORE_DATA:
if watch == nil { if watch == nil {
w.sendError(errors.New("ERROR_MORE_DATA has unexpectedly null lpOverlapped buffer")) w.sendError(errors.New("ERROR_MORE_DATA has unexpectedly null lpOverlapped buffer"))
@ -626,13 +710,12 @@ func (w *Watcher) readEvents() {
default: default:
w.sendError(os.NewSyscallError("GetQueuedCompletionPort", qErr)) w.sendError(os.NewSyscallError("GetQueuedCompletionPort", qErr))
continue continue
case nil:
} }
var offset uint32 var offset uint32
for { for {
if n == 0 { if n == 0 {
w.sendError(errors.New("short read in readEvents()")) w.sendError(ErrEventOverflow)
break break
} }
@ -703,8 +786,9 @@ func (w *Watcher) readEvents() {
// Error! // Error!
if offset >= n { if offset >= n {
//lint:ignore ST1005 Windows should be capitalized
w.sendError(errors.New( w.sendError(errors.New(
"Windows system assumed buffer larger than it is, events have likely been missed.")) "Windows system assumed buffer larger than it is, events have likely been missed"))
break break
} }
} }
@ -720,9 +804,6 @@ func (w *Watcher) toWindowsFlags(mask uint64) uint32 {
if mask&sysFSMODIFY != 0 { if mask&sysFSMODIFY != 0 {
m |= windows.FILE_NOTIFY_CHANGE_LAST_WRITE m |= windows.FILE_NOTIFY_CHANGE_LAST_WRITE
} }
if mask&sysFSATTRIB != 0 {
m |= windows.FILE_NOTIFY_CHANGE_ATTRIBUTES
}
if mask&(sysFSMOVE|sysFSCREATE|sysFSDELETE) != 0 { if mask&(sysFSMOVE|sysFSCREATE|sysFSDELETE) != 0 {
m |= windows.FILE_NOTIFY_CHANGE_FILE_NAME | windows.FILE_NOTIFY_CHANGE_DIR_NAME m |= windows.FILE_NOTIFY_CHANGE_FILE_NAME | windows.FILE_NOTIFY_CHANGE_DIR_NAME
} }

View file

@ -1,13 +1,18 @@
//go:build !plan9
// +build !plan9
// Package fsnotify provides a cross-platform interface for file system // Package fsnotify provides a cross-platform interface for file system
// notifications. // notifications.
//
// Currently supported systems:
//
// Linux 2.6.32+ via inotify
// BSD, macOS via kqueue
// Windows via ReadDirectoryChangesW
// illumos via FEN
package fsnotify package fsnotify
import ( import (
"errors" "errors"
"fmt" "fmt"
"path/filepath"
"strings" "strings"
) )
@ -33,34 +38,52 @@ type Op uint32
// The operations fsnotify can trigger; see the documentation on [Watcher] for a // The operations fsnotify can trigger; see the documentation on [Watcher] for a
// full description, and check them with [Event.Has]. // full description, and check them with [Event.Has].
const ( const (
// A new pathname was created.
Create Op = 1 << iota Create Op = 1 << iota
// The pathname was written to; this does *not* mean the write has finished,
// and a write can be followed by more writes.
Write Write
// The path was removed; any watches on it will be removed. Some "remove"
// operations may trigger a Rename if the file is actually moved (for
// example "remove to trash" is often a rename).
Remove Remove
// The path was renamed to something else; any watched on it will be
// removed.
Rename Rename
// File attributes were changed.
//
// It's generally not recommended to take action on this event, as it may
// get triggered very frequently by some software. For example, Spotlight
// indexing on macOS, anti-virus software, backup software, etc.
Chmod Chmod
) )
// Common errors that can be reported by a watcher // Common errors that can be reported.
var ( var (
ErrNonExistentWatch = errors.New("can't remove non-existent watcher") ErrNonExistentWatch = errors.New("fsnotify: can't remove non-existent watch")
ErrEventOverflow = errors.New("fsnotify queue overflow") ErrEventOverflow = errors.New("fsnotify: queue or buffer overflow")
ErrClosed = errors.New("fsnotify: watcher already closed")
) )
func (op Op) String() string { func (o Op) String() string {
var b strings.Builder var b strings.Builder
if op.Has(Create) { if o.Has(Create) {
b.WriteString("|CREATE") b.WriteString("|CREATE")
} }
if op.Has(Remove) { if o.Has(Remove) {
b.WriteString("|REMOVE") b.WriteString("|REMOVE")
} }
if op.Has(Write) { if o.Has(Write) {
b.WriteString("|WRITE") b.WriteString("|WRITE")
} }
if op.Has(Rename) { if o.Has(Rename) {
b.WriteString("|RENAME") b.WriteString("|RENAME")
} }
if op.Has(Chmod) { if o.Has(Chmod) {
b.WriteString("|CHMOD") b.WriteString("|CHMOD")
} }
if b.Len() == 0 { if b.Len() == 0 {
@ -70,7 +93,7 @@ func (op Op) String() string {
} }
// Has reports if this operation has the given operation. // Has reports if this operation has the given operation.
func (o Op) Has(h Op) bool { return o&h == h } func (o Op) Has(h Op) bool { return o&h != 0 }
// Has reports if this event has the given operation. // Has reports if this event has the given operation.
func (e Event) Has(op Op) bool { return e.Op.Has(op) } func (e Event) Has(op Op) bool { return e.Op.Has(op) }
@ -79,3 +102,45 @@ func (e Event) Has(op Op) bool { return e.Op.Has(op) }
func (e Event) String() string { func (e Event) String() string {
return fmt.Sprintf("%-13s %q", e.Op.String(), e.Name) return fmt.Sprintf("%-13s %q", e.Op.String(), e.Name)
} }
type (
addOpt func(opt *withOpts)
withOpts struct {
bufsize int
}
)
var defaultOpts = withOpts{
bufsize: 65536, // 64K
}
func getOptions(opts ...addOpt) withOpts {
with := defaultOpts
for _, o := range opts {
o(&with)
}
return with
}
// WithBufferSize sets the [ReadDirectoryChangesW] buffer size.
//
// This only has effect on Windows systems, and is a no-op for other backends.
//
// The default value is 64K (65536 bytes) which is the highest value that works
// on all filesystems and should be enough for most applications, but if you
// have a large burst of events it may not be enough. You can increase it if
// you're hitting "queue or buffer overflow" errors ([ErrEventOverflow]).
//
// [ReadDirectoryChangesW]: https://learn.microsoft.com/en-gb/windows/win32/api/winbase/nf-winbase-readdirectorychangesw
func WithBufferSize(bytes int) addOpt {
return func(opt *withOpts) { opt.bufsize = bytes }
}
// Check if this path is recursive (ends with "/..." or "\..."), and return the
// path with the /... stripped.
func recursivePath(path string) (string, bool) {
if filepath.Base(path) == "..." {
return filepath.Dir(path), true
}
return path, false
}

View file

@ -2,8 +2,8 @@
[ "${ZSH_VERSION:-}" = "" ] && echo >&2 "Only works with zsh" && exit 1 [ "${ZSH_VERSION:-}" = "" ] && echo >&2 "Only works with zsh" && exit 1
setopt err_exit no_unset pipefail extended_glob setopt err_exit no_unset pipefail extended_glob
# Simple script to update the godoc comments on all watchers. Probably took me # Simple script to update the godoc comments on all watchers so you don't need
# more time to write this than doing it manually, but ah well 🙃 # to update the same comment 5 times.
watcher=$(<<EOF watcher=$(<<EOF
// Watcher watches a set of paths, delivering events on a channel. // Watcher watches a set of paths, delivering events on a channel.
@ -16,9 +16,9 @@ watcher=$(<<EOF
// When a file is removed a Remove event won't be emitted until all file // When a file is removed a Remove event won't be emitted until all file
// descriptors are closed, and deletes will always emit a Chmod. For example: // descriptors are closed, and deletes will always emit a Chmod. For example:
// //
// fp := os.Open("file") // fp := os.Open("file")
// os.Remove("file") // Triggers Chmod // os.Remove("file") // Triggers Chmod
// fp.Close() // Triggers Remove // fp.Close() // Triggers Remove
// //
// This is the event that inotify sends, so not much can be changed about this. // This is the event that inotify sends, so not much can be changed about this.
// //
@ -32,16 +32,16 @@ watcher=$(<<EOF
// //
// To increase them you can use sysctl or write the value to the /proc file: // To increase them you can use sysctl or write the value to the /proc file:
// //
// # Default values on Linux 5.18 // # Default values on Linux 5.18
// sysctl fs.inotify.max_user_watches=124983 // sysctl fs.inotify.max_user_watches=124983
// sysctl fs.inotify.max_user_instances=128 // sysctl fs.inotify.max_user_instances=128
// //
// To make the changes persist on reboot edit /etc/sysctl.conf or // To make the changes persist on reboot edit /etc/sysctl.conf or
// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check // /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
// your distro's documentation): // your distro's documentation):
// //
// fs.inotify.max_user_watches=124983 // fs.inotify.max_user_watches=124983
// fs.inotify.max_user_instances=128 // fs.inotify.max_user_instances=128
// //
// Reaching the limit will result in a "no space left on device" or "too many open // Reaching the limit will result in a "no space left on device" or "too many open
// files" error. // files" error.
@ -57,14 +57,20 @@ watcher=$(<<EOF
// control the maximum number of open files, as well as /etc/login.conf on BSD // control the maximum number of open files, as well as /etc/login.conf on BSD
// systems. // systems.
// //
// # macOS notes // # Windows notes
// //
// Spotlight indexing on macOS can result in multiple events (see [#15]). A // Paths can be added as "C:\\path\\to\\dir", but forward slashes
// temporary workaround is to add your folder(s) to the "Spotlight Privacy // ("C:/path/to/dir") will also work.
// Settings" until we have a native FSEvents implementation (see [#11]).
// //
// [#11]: https://github.com/fsnotify/fsnotify/issues/11 // When a watched directory is removed it will always send an event for the
// [#15]: https://github.com/fsnotify/fsnotify/issues/15 // directory itself, but may not send events for all files in that directory.
// Sometimes it will send events for all times, sometimes it will send no
// events, and often only for some files.
//
// The default ReadDirectoryChangesW() buffer size is 64K, which is the largest
// value that is guaranteed to work with SMB filesystems. If you have many
// events in quick succession this may not be enough, and you will have to use
// [WithBufferSize] to increase the value.
EOF EOF
) )
@ -73,20 +79,36 @@ new=$(<<EOF
EOF EOF
) )
newbuffered=$(<<EOF
// NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events
// channel.
//
// The main use case for this is situations with a very large number of events
// where the kernel buffer size can't be increased (e.g. due to lack of
// permissions). An unbuffered Watcher will perform better for almost all use
// cases, and whenever possible you will be better off increasing the kernel
// buffers instead of adding a large userspace buffer.
EOF
)
add=$(<<EOF add=$(<<EOF
// Add starts monitoring the path for changes. // Add starts monitoring the path for changes.
// //
// A path can only be watched once; attempting to watch it more than once will // A path can only be watched once; watching it more than once is a no-op and will
// return an error. Paths that do not yet exist on the filesystem cannot be // not return an error. Paths that do not yet exist on the filesystem cannot be
// added. A watch will be automatically removed if the path is deleted. // watched.
// //
// A path will remain watched if it gets renamed to somewhere else on the same // A watch will be automatically removed if the watched path is deleted or
// filesystem, but the monitor will get removed if the path gets deleted and // renamed. The exception is the Windows backend, which doesn't remove the
// re-created, or if it's moved to a different filesystem. // watcher on renames.
// //
// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special // Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
// filesystems (/proc, /sys, etc.) generally don't work. // filesystems (/proc, /sys, etc.) generally don't work.
// //
// Returns [ErrClosed] if [Watcher.Close] was called.
//
// See [Watcher.AddWith] for a version that allows adding options.
//
// # Watching directories // # Watching directories
// //
// All files in a directory are monitored, including new files that are created // All files in a directory are monitored, including new files that are created
@ -96,14 +118,27 @@ add=$(<<EOF
// # Watching files // # Watching files
// //
// Watching individual files (rather than directories) is generally not // Watching individual files (rather than directories) is generally not
// recommended as many tools update files atomically. Instead of "just" writing // recommended as many programs (especially editors) update files atomically: it
// to the file a temporary file will be written to first, and if successful the // will write to a temporary file which is then moved to to destination,
// temporary file is moved to to destination removing the original, or some // overwriting the original (or some variant thereof). The watcher on the
// variant thereof. The watcher on the original file is now lost, as it no // original file is now lost, as that no longer exists.
// longer exists.
// //
// Instead, watch the parent directory and use Event.Name to filter out files // The upshot of this is that a power failure or crash won't leave a
// you're not interested in. There is an example of this in [cmd/fsnotify/file.go]. // half-written file.
//
// Watch the parent directory and use Event.Name to filter out files you're not
// interested in. There is an example of this in cmd/fsnotify/file.go.
EOF
)
addwith=$(<<EOF
// AddWith is like [Watcher.Add], but allows adding options. When using Add()
// the defaults described below are used.
//
// Possible options are:
//
// - [WithBufferSize] sets the buffer size for the Windows backend; no-op on
// other platforms. The default is 64K (65536 bytes).
EOF EOF
) )
@ -114,16 +149,21 @@ remove=$(<<EOF
// /tmp/dir and /tmp/dir/subdir then you will need to remove both. // /tmp/dir and /tmp/dir/subdir then you will need to remove both.
// //
// Removing a path that has not yet been added returns [ErrNonExistentWatch]. // Removing a path that has not yet been added returns [ErrNonExistentWatch].
//
// Returns nil if [Watcher.Close] was called.
EOF EOF
) )
close=$(<<EOF close=$(<<EOF
// Close removes all watches and closes the events channel. // Close removes all watches and closes the Events channel.
EOF EOF
) )
watchlist=$(<<EOF watchlist=$(<<EOF
// WatchList returns all paths added with [Add] (and are not yet removed). // WatchList returns all paths explicitly added with [Watcher.Add] (and are not
// yet removed).
//
// Returns nil if [Watcher.Close] was called.
EOF EOF
) )
@ -153,20 +193,29 @@ events=$(<<EOF
// initiated by the user may show up as one or multiple // initiated by the user may show up as one or multiple
// writes, depending on when the system syncs things to // writes, depending on when the system syncs things to
// disk. For example when compiling a large Go program // disk. For example when compiling a large Go program
// you may get hundreds of Write events, so you // you may get hundreds of Write events, and you may
// probably want to wait until you've stopped receiving // want to wait until you've stopped receiving them
// them (see the dedup example in cmd/fsnotify). // (see the dedup example in cmd/fsnotify).
//
// Some systems may send Write event for directories
// when the directory content changes.
// //
// fsnotify.Chmod Attributes were changed. On Linux this is also sent // fsnotify.Chmod Attributes were changed. On Linux this is also sent
// when a file is removed (or more accurately, when a // when a file is removed (or more accurately, when a
// link to an inode is removed). On kqueue it's sent // link to an inode is removed). On kqueue it's sent
// and on kqueue when a file is truncated. On Windows // when a file is truncated. On Windows it's never
// it's never sent. // sent.
EOF EOF
) )
errors=$(<<EOF errors=$(<<EOF
// Errors sends any errors. // Errors sends any errors.
//
// ErrEventOverflow is used to indicate there are too many events:
//
// - inotify: There are too many queued events (fs.inotify.max_queued_events sysctl)
// - windows: The buffer size is too small; WithBufferSize() can be used to increase it.
// - kqueue, fen: Not used.
EOF EOF
) )
@ -200,7 +249,9 @@ set-cmt() {
set-cmt '^type Watcher struct ' $watcher set-cmt '^type Watcher struct ' $watcher
set-cmt '^func NewWatcher(' $new set-cmt '^func NewWatcher(' $new
set-cmt '^func NewBufferedWatcher(' $newbuffered
set-cmt '^func (w \*Watcher) Add(' $add set-cmt '^func (w \*Watcher) Add(' $add
set-cmt '^func (w \*Watcher) AddWith(' $addwith
set-cmt '^func (w \*Watcher) Remove(' $remove set-cmt '^func (w \*Watcher) Remove(' $remove
set-cmt '^func (w \*Watcher) Close(' $close set-cmt '^func (w \*Watcher) Close(' $close
set-cmt '^func (w \*Watcher) WatchList(' $watchlist set-cmt '^func (w \*Watcher) WatchList(' $watchlist

View file

@ -10,9 +10,6 @@
</h6> </h6>
<p align="center"> <p align="center">
<a href="https://travis-ci.org/gabriel-vasile/mimetype">
<img alt="Build Status" src="https://travis-ci.org/gabriel-vasile/mimetype.svg?branch=master">
</a>
<a href="https://pkg.go.dev/github.com/gabriel-vasile/mimetype"> <a href="https://pkg.go.dev/github.com/gabriel-vasile/mimetype">
<img alt="Go Reference" src="https://pkg.go.dev/badge/github.com/gabriel-vasile/mimetype.svg"> <img alt="Go Reference" src="https://pkg.go.dev/badge/github.com/gabriel-vasile/mimetype.svg">
</a> </a>
@ -30,7 +27,7 @@
## Features ## Features
- fast and precise MIME type and file extension detection - fast and precise MIME type and file extension detection
- long list of [supported MIME types](supported_mimes.md) - long list of [supported MIME types](supported_mimes.md)
- posibility to [extend](https://pkg.go.dev/github.com/gabriel-vasile/mimetype#example-package-Extend) with other file formats - possibility to [extend](https://pkg.go.dev/github.com/gabriel-vasile/mimetype#example-package-Extend) with other file formats
- common file formats are prioritized - common file formats are prioritized
- [text vs. binary files differentiation](https://pkg.go.dev/github.com/gabriel-vasile/mimetype#example-package-TextVsBinary) - [text vs. binary files differentiation](https://pkg.go.dev/github.com/gabriel-vasile/mimetype#example-package-TextVsBinary)
- safe for concurrent usage - safe for concurrent usage

View file

@ -74,13 +74,21 @@ func CRX(raw []byte, limit uint32) bool {
} }
// Tar matches a (t)ape (ar)chive file. // Tar matches a (t)ape (ar)chive file.
//
// Signature source: https://www.nationalarchives.gov.uk/PRONOM/Format/proFormatSearch.aspx?status=detailReport&id=385&strPageToDisplay=signatures
func Tar(raw []byte, _ uint32) bool { func Tar(raw []byte, _ uint32) bool {
// The "magic" header field for files in in UStar (POSIX IEEE P1003.1) archives
// has the prefix "ustar". The values of the remaining bytes in this field vary
// by archiver implementation.
if len(raw) >= 512 && bytes.HasPrefix(raw[257:], []byte{0x75, 0x73, 0x74, 0x61, 0x72}) {
return true
}
if len(raw) < 256 { if len(raw) < 256 {
return false return false
} }
// The older v7 format has no "magic" field, and therefore must be identified
// with heuristics based on legal ranges of values for other header fields:
// https://www.nationalarchives.gov.uk/PRONOM/Format/proFormatSearch.aspx?status=detailReport&id=385&strPageToDisplay=signatures
rules := []struct { rules := []struct {
min, max uint8 min, max uint8
i int i int

View file

@ -150,32 +150,34 @@ func Marc(raw []byte, limit uint32) bool {
} }
// Glb matches a glTF model format file. // Glb matches a glTF model format file.
// GLB is the binary file format representation of 3D models save in // GLB is the binary file format representation of 3D models saved in
// the GL transmission Format (glTF). // the GL transmission Format (glTF).
// see more: https://docs.fileformat.com/3d/glb/ // GLB uses little endian and its header structure is as follows:
// https://www.iana.org/assignments/media-types/model/gltf-binary
// GLB file format is based on little endian and its header structure
// show below:
// //
// <-- 12-byte header --> // <-- 12-byte header -->
// | magic | version | length | // | magic | version | length |
// | (uint32) | (uint32) | (uint32) | // | (uint32) | (uint32) | (uint32) |
// | \x67\x6C\x54\x46 | \x01\x00\x00\x00 | ... | // | \x67\x6C\x54\x46 | \x01\x00\x00\x00 | ... |
// | g l T F | 1 | ... | // | g l T F | 1 | ... |
//
// Visit [glTF specification] and [IANA glTF entry] for more details.
//
// [glTF specification]: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html
// [IANA glTF entry]: https://www.iana.org/assignments/media-types/model/gltf-binary
var Glb = prefix([]byte("\x67\x6C\x54\x46\x02\x00\x00\x00"), var Glb = prefix([]byte("\x67\x6C\x54\x46\x02\x00\x00\x00"),
[]byte("\x67\x6C\x54\x46\x01\x00\x00\x00")) []byte("\x67\x6C\x54\x46\x01\x00\x00\x00"))
// TzIf matches a Time Zone Information Format (TZif) file. // TzIf matches a Time Zone Information Format (TZif) file.
// See more: https://tools.ietf.org/id/draft-murchison-tzdist-tzif-00.html#rfc.section.3 // See more: https://tools.ietf.org/id/draft-murchison-tzdist-tzif-00.html#rfc.section.3
// Its header structure is shown below: // Its header structure is shown below:
// +---------------+---+ // +---------------+---+
// | magic (4) | <-+-- version (1) // | magic (4) | <-+-- version (1)
// +---------------+---+---------------------------------------+ // +---------------+---+---------------------------------------+
// | [unused - reserved for future use] (15) | // | [unused - reserved for future use] (15) |
// +---------------+---------------+---------------+-----------+ // +---------------+---------------+---------------+-----------+
// | isutccnt (4) | isstdcnt (4) | leapcnt (4) | // | isutccnt (4) | isstdcnt (4) | leapcnt (4) |
// +---------------+---------------+---------------+ // +---------------+---------------+---------------+
// | timecnt (4) | typecnt (4) | charcnt (4) | // | timecnt (4) | typecnt (4) | charcnt (4) |
func TzIf(raw []byte, limit uint32) bool { func TzIf(raw []byte, limit uint32) bool {
// File is at least 44 bytes (header size). // File is at least 44 bytes (header size).
if len(raw) < 44 { if len(raw) < 44 {

View file

@ -1,5 +1,7 @@
package magic package magic
import "bytes"
var ( var (
// AVIF matches an AV1 Image File Format still or animated. // AVIF matches an AV1 Image File Format still or animated.
// Wikipedia page seems outdated listing image/avif-sequence for animations. // Wikipedia page seems outdated listing image/avif-sequence for animations.
@ -37,8 +39,6 @@ var (
// Nero Digital AAC Audio // Nero Digital AAC Audio
[]byte("NDAS"), []byte("NDAS"),
) )
// QuickTime matches a QuickTime File Format file.
QuickTime = ftyp([]byte("qt "), []byte("moov"))
// Mqv matches a Sony / Mobile QuickTime file. // Mqv matches a Sony / Mobile QuickTime file.
Mqv = ftyp([]byte("mqt ")) Mqv = ftyp([]byte("mqt "))
// M4a matches an audio M4A file. // M4a matches an audio M4A file.
@ -55,3 +55,34 @@ var (
HeifSequence = ftyp([]byte("msf1"), []byte("hevm"), []byte("hevs"), []byte("avcs")) HeifSequence = ftyp([]byte("msf1"), []byte("hevm"), []byte("hevs"), []byte("avcs"))
// TODO: add support for remaining video formats at ftyps.com. // TODO: add support for remaining video formats at ftyps.com.
) )
// QuickTime matches a QuickTime File Format file.
// https://www.loc.gov/preservation/digital/formats/fdd/fdd000052.shtml
// https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap1/qtff1.html#//apple_ref/doc/uid/TP40000939-CH203-38190
// https://github.com/apache/tika/blob/0f5570691133c75ac4472c3340354a6c4080b104/tika-core/src/main/resources/org/apache/tika/mime/tika-mimetypes.xml#L7758-L7777
func QuickTime(raw []byte, _ uint32) bool {
if len(raw) < 12 {
return false
}
// First 4 bytes represent the size of the atom as unsigned int.
// Next 4 bytes are the type of the atom.
// For `ftyp` atoms check if first byte in size is 0, otherwise, a text file
// which happens to contain 'ftypqt ' at index 4 will trigger a false positive.
if bytes.Equal(raw[4:12], []byte("ftypqt ")) ||
bytes.Equal(raw[4:12], []byte("ftypmoov")) {
return raw[0] == 0x00
}
basicAtomTypes := [][]byte{
[]byte("moov\x00"),
[]byte("mdat\x00"),
[]byte("free\x00"),
[]byte("skip\x00"),
[]byte("pnot\x00"),
}
for _, a := range basicAtomTypes {
if bytes.Equal(raw[4:9], a) {
return true
}
}
return bytes.Equal(raw[:8], []byte("\x00\x00\x00\x08wide"))
}

View file

@ -44,6 +44,10 @@ var (
Hdr = prefix([]byte("#?RADIANCE\n")) Hdr = prefix([]byte("#?RADIANCE\n"))
// Xpm matches X PixMap image data. // Xpm matches X PixMap image data.
Xpm = prefix([]byte{0x2F, 0x2A, 0x20, 0x58, 0x50, 0x4D, 0x20, 0x2A, 0x2F}) Xpm = prefix([]byte{0x2F, 0x2A, 0x20, 0x58, 0x50, 0x4D, 0x20, 0x2A, 0x2F})
// Jxs matches a JPEG XS coded image file (ISO/IEC 21122-3).
Jxs = prefix([]byte{0x00, 0x00, 0x00, 0x0C, 0x4A, 0x58, 0x53, 0x20, 0x0D, 0x0A, 0x87, 0x0A})
// Jxr matches Microsoft HD JXR photo file.
Jxr = prefix([]byte{0x49, 0x49, 0xBC, 0x01})
) )
func jpeg2k(sig []byte) Detector { func jpeg2k(sig []byte) Detector {

View file

@ -177,7 +177,9 @@ func newXMLSig(localName, xmlns string) xmlSig {
// and, optionally, followed by the arguments for the interpreter. // and, optionally, followed by the arguments for the interpreter.
// //
// Ex: // Ex:
// #! /usr/bin/env php //
// #! /usr/bin/env php
//
// /usr/bin/env is the interpreter, php is the first and only argument. // /usr/bin/env is the interpreter, php is the first and only argument.
func shebang(sigs ...[]byte) Detector { func shebang(sigs ...[]byte) Detector {
return func(raw []byte, limit uint32) bool { return func(raw []byte, limit uint32) bool {

View file

@ -3,6 +3,7 @@ package magic
import ( import (
"bytes" "bytes"
"encoding/csv" "encoding/csv"
"errors"
"io" "io"
) )
@ -19,12 +20,23 @@ func Tsv(raw []byte, limit uint32) bool {
func sv(in []byte, comma rune, limit uint32) bool { func sv(in []byte, comma rune, limit uint32) bool {
r := csv.NewReader(dropLastLine(in, limit)) r := csv.NewReader(dropLastLine(in, limit))
r.Comma = comma r.Comma = comma
r.TrimLeadingSpace = true r.ReuseRecord = true
r.LazyQuotes = true r.LazyQuotes = true
r.Comment = '#' r.Comment = '#'
lines, err := r.ReadAll() lines := 0
return err == nil && r.FieldsPerRecord > 1 && len(lines) > 1 for {
_, err := r.Read()
if errors.Is(err, io.EOF) {
break
}
if err != nil {
return false
}
lines++
}
return r.FieldsPerRecord > 1 && lines > 1
} }
// dropLastLine drops the last incomplete line from b. // dropLastLine drops the last incomplete line from b.

View file

@ -139,7 +139,7 @@ func (m *MIME) clone(ps map[string]string) *MIME {
} }
// cloneHierarchy creates a clone of m and all its ancestors. The optional MIME // cloneHierarchy creates a clone of m and all its ancestors. The optional MIME
// parametes are set on the last child of the hierarchy. // parameters are set on the last child of the hierarchy.
func (m *MIME) cloneHierarchy(ps map[string]string) *MIME { func (m *MIME) cloneHierarchy(ps map[string]string) *MIME {
ret := m.clone(ps) ret := m.clone(ps)
lastChild := ret lastChild := ret

View file

@ -39,7 +39,8 @@ func Detect(in []byte) *MIME {
// //
// DetectReader assumes the reader offset is at the start. If the input is an // DetectReader assumes the reader offset is at the start. If the input is an
// io.ReadSeeker you previously read from, it should be rewinded before detection: // io.ReadSeeker you previously read from, it should be rewinded before detection:
// reader.Seek(0, io.SeekStart) //
// reader.Seek(0, io.SeekStart)
func DetectReader(r io.Reader) (*MIME, error) { func DetectReader(r io.Reader) (*MIME, error) {
var in []byte var in []byte
var err error var err error

View file

@ -1,4 +1,4 @@
## 171 Supported MIME types ## 173 Supported MIME types
This file is automatically generated when running tests. Do not edit manually. This file is automatically generated when running tests. Do not edit manually.
Extension | MIME type | Aliases Extension | MIME type | Aliases
@ -46,6 +46,7 @@ Extension | MIME type | Aliases
**.jp2** | image/jp2 | - **.jp2** | image/jp2 | -
**.jpf** | image/jpx | - **.jpf** | image/jpx | -
**.jpm** | image/jpm | video/jpm **.jpm** | image/jpm | video/jpm
**.jxs** | image/jxs | -
**.gif** | image/gif | - **.gif** | image/gif | -
**.webp** | image/webp | - **.webp** | image/webp | -
**.exe** | application/vnd.microsoft.portable-executable | - **.exe** | application/vnd.microsoft.portable-executable | -
@ -139,6 +140,7 @@ Extension | MIME type | Aliases
**.glb** | model/gltf-binary | - **.glb** | model/gltf-binary | -
**.avif** | image/avif | - **.avif** | image/avif | -
**.cab** | application/x-installshield | - **.cab** | application/x-installshield | -
**.jxr** | image/jxr | image/vnd.ms-photo
**.txt** | text/plain | - **.txt** | text/plain | -
**.html** | text/html | - **.html** | text/html | -
**.svg** | image/svg+xml | - **.svg** | image/svg+xml | -

View file

@ -18,13 +18,13 @@ import (
var root = newMIME("application/octet-stream", "", var root = newMIME("application/octet-stream", "",
func([]byte, uint32) bool { return true }, func([]byte, uint32) bool { return true },
xpm, sevenZ, zip, pdf, fdf, ole, ps, psd, p7s, ogg, png, jpg, jxl, jp2, jpx, xpm, sevenZ, zip, pdf, fdf, ole, ps, psd, p7s, ogg, png, jpg, jxl, jp2, jpx,
jpm, gif, webp, exe, elf, ar, tar, xar, bz2, fits, tiff, bmp, ico, mp3, flac, jpm, jxs, gif, webp, exe, elf, ar, tar, xar, bz2, fits, tiff, bmp, ico, mp3, flac,
midi, ape, musePack, amr, wav, aiff, au, mpeg, quickTime, mqv, mp4, webM, midi, ape, musePack, amr, wav, aiff, au, mpeg, quickTime, mqv, mp4, webM,
threeGP, threeG2, avi, flv, mkv, asf, aac, voc, aMp4, m4a, m3u, m4v, rmvb, threeGP, threeG2, avi, flv, mkv, asf, aac, voc, aMp4, m4a, m3u, m4v, rmvb,
gzip, class, swf, crx, ttf, woff, woff2, otf, ttc, eot, wasm, shx, dbf, dcm, rar, gzip, class, swf, crx, ttf, woff, woff2, otf, ttc, eot, wasm, shx, dbf, dcm, rar,
djvu, mobi, lit, bpg, sqlite3, dwg, nes, lnk, macho, qcp, icns, heic, djvu, mobi, lit, bpg, sqlite3, dwg, nes, lnk, macho, qcp, icns, heic,
heicSeq, heif, heifSeq, hdr, mrc, mdb, accdb, zstd, cab, rpm, xz, lzip, heicSeq, heif, heifSeq, hdr, mrc, mdb, accdb, zstd, cab, rpm, xz, lzip,
torrent, cpio, tzif, xcf, pat, gbr, glb, avif, cabIS, torrent, cpio, tzif, xcf, pat, gbr, glb, avif, cabIS, jxr,
// Keep text last because it is the slowest check // Keep text last because it is the slowest check
text, text,
) )
@ -34,7 +34,7 @@ var root = newMIME("application/octet-stream", "",
// errMIME is same as root but it does not require locking. // errMIME is same as root but it does not require locking.
var errMIME = newMIME("application/octet-stream", "", func([]byte, uint32) bool { return false }) var errMIME = newMIME("application/octet-stream", "", func([]byte, uint32) bool { return false })
// mu guards access to the root MIME tree. Access to root must be synchonized with this lock. // mu guards access to the root MIME tree. Access to root must be synchronized with this lock.
var mu = &sync.RWMutex{} var mu = &sync.RWMutex{}
// The list of nodes appended to the root node. // The list of nodes appended to the root node.
@ -122,6 +122,7 @@ var (
jpx = newMIME("image/jpx", ".jpf", magic.Jpx) jpx = newMIME("image/jpx", ".jpf", magic.Jpx)
jpm = newMIME("image/jpm", ".jpm", magic.Jpm). jpm = newMIME("image/jpm", ".jpm", magic.Jpm).
alias("video/jpm") alias("video/jpm")
jxs = newMIME("image/jxs", ".jxs", magic.Jxs)
xpm = newMIME("image/x-xpixmap", ".xpm", magic.Xpm) xpm = newMIME("image/x-xpixmap", ".xpm", magic.Xpm)
bpg = newMIME("image/bpg", ".bpg", magic.Bpg) bpg = newMIME("image/bpg", ".bpg", magic.Bpg)
gif = newMIME("image/gif", ".gif", magic.Gif) gif = newMIME("image/gif", ".gif", magic.Gif)
@ -255,4 +256,5 @@ var (
gbr = newMIME("image/x-gimp-gbr", ".gbr", magic.Gbr) gbr = newMIME("image/x-gimp-gbr", ".gbr", magic.Gbr)
xfdf = newMIME("application/vnd.adobe.xfdf", ".xfdf", magic.Xfdf) xfdf = newMIME("application/vnd.adobe.xfdf", ".xfdf", magic.Xfdf)
glb = newMIME("model/gltf-binary", ".glb", magic.Glb) glb = newMIME("model/gltf-binary", ".glb", magic.Glb)
jxr = newMIME("image/jxr", ".jxr", magic.Jxr).alias("image/vnd.ms-photo")
) )

13
vendor/github.com/getsentry/sentry-go/.codecov.yml generated vendored Normal file
View file

@ -0,0 +1,13 @@
codecov:
# across
notify:
# Do not notify until at least this number of reports have been uploaded
# from the CI pipeline. We normally have more than that number, but 6
# should be enough to get a first notification.
after_n_builds: 6
coverage:
status:
project:
default:
# Do not fail the commit status if the coverage was reduced up to this value
threshold: 0.5%

View file

@ -1,12 +1,13 @@
minVersion: 0.23.1 minVersion: 0.35.0
preReleaseCommand: bash scripts/craft-pre-release.sh
changelogPolicy: simple changelogPolicy: simple
artifactProvider: artifactProvider:
name: none name: none
targets: targets:
- name: github - name: github
includeNames: /none/
tagPrefix: v tagPrefix: v
- name: github
tagPrefix: otel/v
tagOnly: true
- name: registry - name: registry
sdks: sdks:
github:getsentry/sentry-go: github:getsentry/sentry-go:

View file

@ -1,6 +1,14 @@
# Code coverage artifacts
coverage.txt coverage.txt
coverage.out
coverage.html
.coverage/
# Just my personal way of tracking stuff — Kamil # Just my personal way of tracking stuff — Kamil
FIXME.md FIXME.md
TODO.md TODO.md
!NOTES.md !NOTES.md
# IDE system files
.idea
.vscode

View file

@ -2,8 +2,6 @@ linters:
disable-all: true disable-all: true
enable: enable:
- bodyclose - bodyclose
- deadcode
- depguard
- dogsled - dogsled
- dupl - dupl
- errcheck - errcheck
@ -24,17 +22,16 @@ linters:
- prealloc - prealloc
- revive - revive
- staticcheck - staticcheck
- structcheck
- typecheck - typecheck
- unconvert - unconvert
- unparam - unparam
- unused - unused
- varcheck
- whitespace - whitespace
issues: issues:
exclude-rules: exclude-rules:
- path: _test\.go - path: _test\.go
linters: linters:
- goconst
- prealloc - prealloc
- path: _test\.go - path: _test\.go
text: "G306:" text: "G306:"

View file

@ -1,5 +1,469 @@
# Changelog # Changelog
## 0.27.0
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.27.0.
### Breaking Changes
- `Exception.ThreadId` is now typed as `uint64`. It was wrongly typed as `string` before. ([#770](https://github.com/getsentry/sentry-go/pull/770))
### Misc
- Export `Event.Attachments` ([#771](https://github.com/getsentry/sentry-go/pull/771))
## 0.26.0
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.26.0.
### Breaking Changes
As previously announced, this release removes some methods from the SDK.
- `sentry.TransactionName()` use `sentry.WithTransactionName()` instead.
- `sentry.OpName()` use `sentry.WithOpName()` instead.
- `sentry.TransctionSource()` use `sentry.WithTransactionSource()` instead.
- `sentry.SpanSampled()` use `sentry.WithSpanSampled()` instead.
### Features
- Add `WithDescription` span option ([#751](https://github.com/getsentry/sentry-go/pull/751))
```go
span := sentry.StartSpan(ctx, "http.client", WithDescription("GET /api/users"))
```
- Add support for package name parsing in Go 1.20 and higher ([#730](https://github.com/getsentry/sentry-go/pull/730))
### Bug Fixes
- Apply `ClientOptions.SampleRate` only to errors & messages ([#754](https://github.com/getsentry/sentry-go/pull/754))
- Check if git is available before executing any git commands ([#737](https://github.com/getsentry/sentry-go/pull/737))
## 0.25.0
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.25.0.
### Breaking Changes
As previously announced, this release removes two global constants from the SDK.
- `sentry.Version` was removed. Use `sentry.SDKVersion` instead ([#727](https://github.com/getsentry/sentry-go/pull/727))
- `sentry.SDKIdentifier` was removed. Use `Client.GetSDKIdentifier()` instead ([#727](https://github.com/getsentry/sentry-go/pull/727))
### Features
- Add `ClientOptions.IgnoreTransactions`, which allows you to ignore specific transactions based on their name ([#717](https://github.com/getsentry/sentry-go/pull/717))
- Add `ClientOptions.Tags`, which allows you to set global tags that are applied to all events. You can also define tags by setting `SENTRY_TAGS_` environment variables ([#718](https://github.com/getsentry/sentry-go/pull/718))
### Bug fixes
- Fix an issue in the profiler that would cause an infinite loop if the duration of a transaction is longer than 30 seconds ([#724](https://github.com/getsentry/sentry-go/issues/724))
### Misc
- `dsn.RequestHeaders()` is not to be removed, though it is still considered deprecated and should only be used when using a custom transport that sends events to the `/store` endpoint ([#720](https://github.com/getsentry/sentry-go/pull/720))
## 0.24.1
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.24.1.
### Bug fixes
- Prevent a panic in `sentryotel.flushSpanProcessor()` ([(#711)](https://github.com/getsentry/sentry-go/pull/711))
- Prevent a panic when setting the SDK identifier ([#715](https://github.com/getsentry/sentry-go/pull/715))
## 0.24.0
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.24.0.
### Deprecations
- `sentry.Version` to be removed in 0.25.0. Use `sentry.SDKVersion` instead.
- `sentry.SDKIdentifier` to be removed in 0.25.0. Use `Client.GetSDKIdentifier()` instead.
- `dsn.RequestHeaders()` to be removed after 0.25.0, but no earlier than December 1, 2023. Requests to the `/envelope` endpoint are authenticated using the DSN in the envelope header.
### Features
- Run a single instance of the profiler instead of multiple ones for each Go routine ([#655](https://github.com/getsentry/sentry-go/pull/655))
- Use the route path as the transaction names when using the Gin integration ([#675](https://github.com/getsentry/sentry-go/pull/675))
- Set the SDK name accordingly when a framework integration is used ([#694](https://github.com/getsentry/sentry-go/pull/694))
- Read release information (VCS revision) from `debug.ReadBuildInfo` ([#704](https://github.com/getsentry/sentry-go/pull/704))
### Bug fixes
- [otel] Fix incorrect usage of `attributes.Value.AsString` ([#684](https://github.com/getsentry/sentry-go/pull/684))
- Fix trace function name parsing in profiler on go1.21+ ([#695](https://github.com/getsentry/sentry-go/pull/695))
### Misc
- Test against Go 1.21 ([#695](https://github.com/getsentry/sentry-go/pull/695))
- Make tests more robust ([#698](https://github.com/getsentry/sentry-go/pull/698), [#699](https://github.com/getsentry/sentry-go/pull/699), [#700](https://github.com/getsentry/sentry-go/pull/700), [#702](https://github.com/getsentry/sentry-go/pull/702))
## 0.23.0
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.23.0.
### Features
- Initial support for [Cron Monitoring](https://docs.sentry.io/product/crons/) ([#661](https://github.com/getsentry/sentry-go/pull/661))
This is how the basic usage of the feature looks like:
```go
// 🟡 Notify Sentry your job is running:
checkinId := sentry.CaptureCheckIn(
&sentry.CheckIn{
MonitorSlug: "<monitor-slug>",
Status: sentry.CheckInStatusInProgress,
},
nil,
)
// Execute your scheduled task here...
// 🟢 Notify Sentry your job has completed successfully:
sentry.CaptureCheckIn(
&sentry.CheckIn{
ID: *checkinId,
MonitorSlug: "<monitor-slug>",
Status: sentry.CheckInStatusOK,
},
nil,
)
```
A full example of using Crons Monitoring is available [here](https://github.com/getsentry/sentry-go/blob/dde4d360660838f3c2e0ced8205bc8f7a8d312d9/_examples/crons/main.go).
More documentation on configuring and using Crons [can be found here](https://docs.sentry.io/platforms/go/crons/).
- Add support for [Event Attachments](https://docs.sentry.io/platforms/go/enriching-events/attachments/) ([#670](https://github.com/getsentry/sentry-go/pull/670))
It's now possible to add file/binary payloads to Sentry events:
```go
sentry.ConfigureScope(func(scope *sentry.Scope) {
scope.AddAttachment(&Attachment{
Filename: "report.html",
ContentType: "text/html",
Payload: []byte("<h1>Look, HTML</h1>"),
})
})
```
The attachment will then be accessible on the Issue Details page.
- Add sampling decision to trace envelope header ([#666](https://github.com/getsentry/sentry-go/pull/666))
- Expose SpanFromContext function ([#672](https://github.com/getsentry/sentry-go/pull/672))
### Bug fixes
- Make `Span.Finish` a no-op when the span is already finished ([#660](https://github.com/getsentry/sentry-go/pull/660))
## 0.22.0
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.22.0.
This release contains initial [profiling](https://docs.sentry.io/product/profiling/) support, as well as a few bug fixes and improvements.
### Features
- Initial (alpha) support for [profiling](https://docs.sentry.io/product/profiling/) ([#626](https://github.com/getsentry/sentry-go/pull/626))
Profiling is disabled by default. To enable it, configure both `TracesSampleRate` and `ProfilesSampleRate` when initializing the SDK:
```go
err := sentry.Init(sentry.ClientOptions{
Dsn: "__DSN__",
EnableTracing: true,
TracesSampleRate: 1.0,
// The sampling rate for profiling is relative to TracesSampleRate. In this case, we'll capture profiles for 100% of transactions.
ProfilesSampleRate: 1.0,
})
```
More documentation on profiling and current limitations [can be found here](https://docs.sentry.io/platforms/go/profiling/).
- Add transactions/tracing support go the Gin integration ([#644](https://github.com/getsentry/sentry-go/pull/644))
### Bug fixes
- Always set a valid source on transactions ([#637](https://github.com/getsentry/sentry-go/pull/637))
- Clone scope.Context in more places to avoid panics on concurrent reads and writes ([#638](https://github.com/getsentry/sentry-go/pull/638))
- Fixes [#570](https://github.com/getsentry/sentry-go/issues/570)
- Fix frames recognized as not being in-app still showing as in-app ([#647](https://github.com/getsentry/sentry-go/pull/647))
## 0.21.0
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.21.0.
Note: this release includes one **breaking change** and some **deprecations**, which are listed below.
### Breaking Changes
**This change does not apply if you use [https://sentry.io](https://sentry.io)**
- Remove support for the `/store` endpoint ([#631](https://github.com/getsentry/sentry-go/pull/631))
- This change requires a self-hosted version of Sentry 20.6.0 or higher. If you are using a version of [self-hosted Sentry](https://develop.sentry.dev/self-hosted/) (aka *on-premise*) older than 20.6.0, then you will need to [upgrade](https://develop.sentry.dev/self-hosted/releases/) your instance.
### Features
- Rename four span option functions ([#611](https://github.com/getsentry/sentry-go/pull/611), [#624](https://github.com/getsentry/sentry-go/pull/624))
- `TransctionSource` -> `WithTransactionSource`
- `SpanSampled` -> `WithSpanSampled`
- `OpName` -> `WithOpName`
- `TransactionName` -> `WithTransactionName`
- Old functions `TransctionSource`, `SpanSampled`, `OpName`, and `TransactionName` are still available but are now **deprecated** and will be removed in a future release.
- Make `client.EventFromMessage` and `client.EventFromException` methods public ([#607](https://github.com/getsentry/sentry-go/pull/607))
- Add `client.SetException` method ([#607](https://github.com/getsentry/sentry-go/pull/607))
- This allows to set or add errors to an existing `Event`.
### Bug Fixes
- Protect from panics while doing concurrent reads/writes to Span data fields ([#609](https://github.com/getsentry/sentry-go/pull/609))
- [otel] Improve detection of Sentry-related spans ([#632](https://github.com/getsentry/sentry-go/pull/632), [#636](https://github.com/getsentry/sentry-go/pull/636))
- Fixes cases when HTTP spans containing requests to Sentry were captured by Sentry ([#627](https://github.com/getsentry/sentry-go/issues/627))
### Misc
- Drop testing in (legacy) GOPATH mode ([#618](https://github.com/getsentry/sentry-go/pull/618))
- Remove outdated documentation from https://pkg.go.dev/github.com/getsentry/sentry-go ([#623](https://github.com/getsentry/sentry-go/pull/623))
## 0.20.0
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.20.0.
Note: this release has some **breaking changes**, which are listed below.
### Breaking Changes
- Remove the following methods: `Scope.SetTransaction()`, `Scope.Transaction()` ([#605](https://github.com/getsentry/sentry-go/pull/605))
Span.Name should be used instead to access the transaction's name.
For example, the following [`TracesSampler`](https://docs.sentry.io/platforms/go/configuration/sampling/#setting-a-sampling-function) function should be now written as follows:
**Before:**
```go
TracesSampler: func(ctx sentry.SamplingContext) float64 {
hub := sentry.GetHubFromContext(ctx.Span.Context())
if hub.Scope().Transaction() == "GET /health" {
return 0
}
return 1
},
```
**After:**
```go
TracesSampler: func(ctx sentry.SamplingContext) float64 {
if ctx.Span.Name == "GET /health" {
return 0
}
return 1
},
```
### Features
- Add `Span.SetContext()` method ([#599](https://github.com/getsentry/sentry-go/pull/599/))
- It is recommended to use it instead of `hub.Scope().SetContext` when setting or updating context on transactions.
- Add `DebugMeta` interface to `Event` and extend `Frame` structure with more fields ([#606](https://github.com/getsentry/sentry-go/pull/606))
- More about DebugMeta interface [here](https://develop.sentry.dev/sdk/event-payloads/debugmeta/).
### Bug Fixes
- [otel] Fix missing OpenTelemetry context on some events ([#599](https://github.com/getsentry/sentry-go/pull/599), [#605](https://github.com/getsentry/sentry-go/pull/605))
- Fixes ([#596](https://github.com/getsentry/sentry-go/issues/596)).
- [otel] Better handling for HTTP span attributes ([#610](https://github.com/getsentry/sentry-go/pull/610))
### Misc
- Bump minimum versions: `github.com/kataras/iris/v12` to 12.2.0, `github.com/labstack/echo/v4` to v4.10.0 ([#595](https://github.com/getsentry/sentry-go/pull/595))
- Resolves [GO-2022-1144 / CVE-2022-41717](https://deps.dev/advisory/osv/GO-2022-1144), [GO-2023-1495 / CVE-2022-41721](https://deps.dev/advisory/osv/GO-2023-1495), [GO-2022-1059 / CVE-2022-32149](https://deps.dev/advisory/osv/GO-2022-1059).
- Bump `google.golang.org/protobuf` minimum required version to 1.29.1 ([#604](https://github.com/getsentry/sentry-go/pull/604))
- This fixes a potential denial of service issue ([CVE-2023-24535](https://github.com/advisories/GHSA-hw7c-3rfg-p46j)).
- Exclude the `otel` module when building in GOPATH mode ([#615](https://github.com/getsentry/sentry-go/pull/615))
## 0.19.0
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.19.0.
### Features
- Add support for exception mechanism metadata ([#564](https://github.com/getsentry/sentry-go/pull/564/))
- More about exception mechanisms [here](https://develop.sentry.dev/sdk/event-payloads/exception/#exception-mechanism).
### Bug Fixes
- [otel] Use the correct "trace" context when sending a Sentry error ([#580](https://github.com/getsentry/sentry-go/pull/580/))
### Misc
- Drop support for Go 1.17, add support for Go 1.20 ([#563](https://github.com/getsentry/sentry-go/pull/563/))
- According to our policy, we're officially supporting the last three minor releases of Go.
- Switch repository license to MIT ([#583](https://github.com/getsentry/sentry-go/pull/583/))
- More about Sentry licensing [here](https://open.sentry.io/licensing/).
- Bump `golang.org/x/text` minimum required version to 0.3.8 ([#586](https://github.com/getsentry/sentry-go/pull/586))
- This fixes [CVE-2022-32149](https://github.com/advisories/GHSA-69ch-w2m2-3vjp) vulnerability.
## 0.18.0
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.18.0.
This release contains initial support for [OpenTelemetry](https://opentelemetry.io/) and various other bug fixes and improvements.
**Note**: This is the last release supporting Go 1.17.
### Features
- Initial support for [OpenTelemetry](https://opentelemetry.io/).
You can now send all your OpenTelemetry spans to Sentry.
Install the `otel` module
```bash
go get github.com/getsentry/sentry-go \
github.com/getsentry/sentry-go/otel
```
Configure the Sentry and OpenTelemetry SDKs
```go
import (
"go.opentelemetry.io/otel"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"github.com/getsentry/sentry-go"
"github.com/getsentry/sentry-go/otel"
// ...
)
// Initlaize the Sentry SDK
sentry.Init(sentry.ClientOptions{
Dsn: "__DSN__",
EnableTracing: true,
TracesSampleRate: 1.0,
})
// Set up the Sentry span processor
tp := sdktrace.NewTracerProvider(
sdktrace.WithSpanProcessor(sentryotel.NewSentrySpanProcessor()),
// ...
)
otel.SetTracerProvider(tp)
// Set up the Sentry propagator
otel.SetTextMapPropagator(sentryotel.NewSentryPropagator())
```
You can read more about using OpenTelemetry with Sentry in our [docs](https://docs.sentry.io/platforms/go/performance/instrumentation/opentelemetry/).
### Bug Fixes
- Do not freeze the Dynamic Sampling Context when no Sentry values are present in the baggage header ([#532](https://github.com/getsentry/sentry-go/pull/532))
- Create a frozen Dynamic Sampling Context when calling `span.ToBaggage()` ([#566](https://github.com/getsentry/sentry-go/pull/566))
- Fix baggage parsing and encoding in vendored otel package ([#568](https://github.com/getsentry/sentry-go/pull/568))
### Misc
- Add `Span.SetDynamicSamplingContext()` ([#539](https://github.com/getsentry/sentry-go/pull/539/))
- Add various getters for `Dsn` ([#540](https://github.com/getsentry/sentry-go/pull/540))
- Add `SpanOption::SpanSampled` ([#546](https://github.com/getsentry/sentry-go/pull/546))
- Add `Span.SetData()` ([#542](https://github.com/getsentry/sentry-go/pull/542))
- Add `Span.IsTransaction()` ([#543](https://github.com/getsentry/sentry-go/pull/543))
- Add `Span.GetTransaction()` method ([#558](https://github.com/getsentry/sentry-go/pull/558))
## 0.17.0
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.17.0.
This release contains a new `BeforeSendTransaction` hook option and corrects two regressions introduced in `0.16.0`.
### Features
- Add `BeforeSendTransaction` hook to `ClientOptions` ([#517](https://github.com/getsentry/sentry-go/pull/517))
- Here's [an example](https://github.com/getsentry/sentry-go/blob/master/_examples/http/main.go#L56-L66) of how BeforeSendTransaction can be used to modify or drop transaction events.
### Bug Fixes
- Do not crash in Span.Finish() when the Client is empty [#520](https://github.com/getsentry/sentry-go/pull/520)
- Fixes [#518](https://github.com/getsentry/sentry-go/issues/518)
- Attach non-PII/non-sensitive request headers to events when `ClientOptions.SendDefaultPii` is set to `false` ([#524](https://github.com/getsentry/sentry-go/pull/524))
- Fixes [#523](https://github.com/getsentry/sentry-go/issues/523)
### Misc
- Clarify how to handle logrus.Fatalf events ([#501](https://github.com/getsentry/sentry-go/pull/501/))
- Rename the `examples` directory to `_examples` ([#521](https://github.com/getsentry/sentry-go/pull/521))
- This removes an indirect dependency to `github.com/golang-jwt/jwt`
## 0.16.0
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.16.0.
Due to ongoing work towards a stable API for `v1.0.0`, we sadly had to include **two breaking changes** in this release.
### Breaking Changes
- Add `EnableTracing`, a boolean option flag to enable performance monitoring (`false` by default).
- If you're using `TracesSampleRate` or `TracesSampler`, this option is **required** to enable performance monitoring.
```go
sentry.Init(sentry.ClientOptions{
EnableTracing: true,
TracesSampleRate: 1.0,
})
```
- Unify TracesSampler [#498](https://github.com/getsentry/sentry-go/pull/498)
- `TracesSampler` was changed to a callback that must return a `float64` between `0.0` and `1.0`.
For example, you can apply a sample rate of `1.0` (100%) to all `/api` transactions, and a sample rate of `0.5` (50%) to all other transactions.
You can read more about this in our [SDK docs](https://docs.sentry.io/platforms/go/configuration/filtering/#using-sampling-to-filter-transaction-events).
```go
sentry.Init(sentry.ClientOptions{
TracesSampler: sentry.TracesSampler(func(ctx sentry.SamplingContext) float64 {
hub := sentry.GetHubFromContext(ctx.Span.Context())
name := hub.Scope().Transaction()
if strings.HasPrefix(name, "GET /api") {
return 1.0
}
return 0.5
}),
}
```
### Features
- Send errors logged with [Logrus](https://github.com/sirupsen/logrus) to Sentry.
- Have a look at our [logrus examples](https://github.com/getsentry/sentry-go/blob/master/_examples/logrus/main.go) on how to use the integration.
- Add support for Dynamic Sampling [#491](https://github.com/getsentry/sentry-go/pull/491)
- You can read more about Dynamic Sampling in our [product docs](https://docs.sentry.io/product/data-management-settings/dynamic-sampling/).
- Add detailed logging about the reason transactions are being dropped.
- You can enable SDK logging via `sentry.ClientOptions.Debug: true`.
### Bug Fixes
- Do not clone the hub when calling `StartTransaction` [#505](https://github.com/getsentry/sentry-go/pull/505)
- Fixes [#502](https://github.com/getsentry/sentry-go/issues/502)
## 0.15.0
- fix: Scope values should not override Event values (#446)
- feat: Make maximum amount of spans configurable (#460)
- feat: Add a method to start a transaction (#482)
- feat: Extend User interface by adding Data, Name and Segment (#483)
- feat: Add ClientOptions.SendDefaultPII (#485)
## 0.14.0
- feat: Add function to continue from trace string (#434)
- feat: Add `max-depth` options (#428)
- *[breaking]* ref: Use a `Context` type mapping to a `map[string]interface{}` for all event contexts (#444)
- *[breaking]* ref: Replace deprecated `ioutil` pkg with `os` & `io` (#454)
- ref: Optimize `stacktrace.go` from size and speed (#467)
- ci: Test against `go1.19` and `go1.18`, drop `go1.16` and `go1.15` support (#432, #477)
- deps: Dependency update to fix CVEs (#462, #464, #477)
_NOTE:_ This version drops support for Go 1.16 and Go 1.15. The currently supported Go versions are the last 3 stable releases: 1.19, 1.18 and 1.17.
## v0.13.0 ## v0.13.0
- ref: Change DSN ProjectID to be a string (#420) - ref: Change DSN ProjectID to be a string (#420)
@ -57,7 +521,7 @@ There are no breaking changes and upgrading should be a smooth experience for al
_NOTE:_ _NOTE:_
This version introduces support for [Sentry's Performance Monitoring](https://docs.sentry.io/platforms/go/performance/). This version introduces support for [Sentry's Performance Monitoring](https://docs.sentry.io/platforms/go/performance/).
The new tracing capabilities are beta, and we plan to expand them on future versions. Feedback is welcome, please open new issues on GitHub. The new tracing capabilities are beta, and we plan to expand them on future versions. Feedback is welcome, please open new issues on GitHub.
The `sentryhttp` package got better API docs, an [updated usage example](https://github.com/getsentry/sentry-go/tree/master/example/http) and support for creating automatic transactions as part of Performance Monitoring. The `sentryhttp` package got better API docs, an [updated usage example](https://github.com/getsentry/sentry-go/tree/master/_examples/http) and support for creating automatic transactions as part of Performance Monitoring.
## v0.8.0 ## v0.8.0

View file

@ -72,6 +72,8 @@ $ go test -race -coverprofile=coverage.txt -covermode=atomic && go tool cover -h
## Linting ## Linting
Lint with [`golangci-lint`](https://github.com/golangci/golangci-lint):
```console ```console
$ golangci-lint run $ golangci-lint run
``` ```

View file

@ -1,9 +1,21 @@
Copyright (c) 2019 Sentry (https://sentry.io) and individual contributors. MIT License
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Copyright (c) 2019 Functional Software, Inc. dba Sentry
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Permission is hereby granted, free of charge, to any person obtaining a copy
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 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:
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 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.

83
vendor/github.com/getsentry/sentry-go/Makefile generated vendored Normal file
View file

@ -0,0 +1,83 @@
.DEFAULT_GOAL := help
MKFILE_PATH := $(abspath $(lastword $(MAKEFILE_LIST)))
MKFILE_DIR := $(dir $(MKFILE_PATH))
ALL_GO_MOD_DIRS := $(shell find . -type f -name 'go.mod' -exec dirname {} \; | sort)
GO = go
TIMEOUT = 300
# Parse Makefile and display the help
help: ## Show help
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
.PHONY: help
build: ## Build everything
for dir in $(ALL_GO_MOD_DIRS); do \
cd "$${dir}"; \
echo ">>> Running 'go build' for module: $${dir}"; \
go build ./...; \
done;
.PHONY: build
### Tests (inspired by https://github.com/open-telemetry/opentelemetry-go/blob/main/Makefile)
TEST_TARGETS := test-short test-verbose test-race
test-race: ARGS=-race
test-short: ARGS=-short
test-verbose: ARGS=-v -race
$(TEST_TARGETS): test
test: $(ALL_GO_MOD_DIRS:%=test/%) ## Run tests
test/%: DIR=$*
test/%:
@echo ">>> Running tests for module: $(DIR)"
@# We use '-count=1' to disable test caching.
(cd $(DIR) && $(GO) test -count=1 -timeout $(TIMEOUT)s $(ARGS) ./...)
.PHONY: $(TEST_TARGETS) test
# Coverage
COVERAGE_MODE = atomic
COVERAGE_PROFILE = coverage.out
COVERAGE_REPORT_DIR = .coverage
COVERAGE_REPORT_DIR_ABS = "$(MKFILE_DIR)/$(COVERAGE_REPORT_DIR)"
$(COVERAGE_REPORT_DIR):
mkdir -p $(COVERAGE_REPORT_DIR)
clean-report-dir: $(COVERAGE_REPORT_DIR)
test $(COVERAGE_REPORT_DIR) && rm -f $(COVERAGE_REPORT_DIR)/*
test-coverage: $(COVERAGE_REPORT_DIR) clean-report-dir ## Test with coverage enabled
set -e ; \
for dir in $(ALL_GO_MOD_DIRS); do \
echo ">>> Running tests with coverage for module: $${dir}"; \
DIR_ABS=$$(python -c 'import os, sys; print(os.path.realpath(sys.argv[1]))' $${dir}) ; \
REPORT_NAME=$$(basename $${DIR_ABS}); \
(cd "$${dir}" && \
$(GO) test -count=1 -timeout $(TIMEOUT)s -coverpkg=./... -covermode=$(COVERAGE_MODE) -coverprofile="$(COVERAGE_PROFILE)" ./... && \
cp $(COVERAGE_PROFILE) "$(COVERAGE_REPORT_DIR_ABS)/$${REPORT_NAME}_$(COVERAGE_PROFILE)" && \
$(GO) tool cover -html=$(COVERAGE_PROFILE) -o coverage.html); \
done;
.PHONY: test-coverage clean-report-dir
mod-tidy: ## Check go.mod tidiness
set -e ; \
for dir in $(ALL_GO_MOD_DIRS); do \
cd "$${dir}"; \
echo ">>> Running 'go mod tidy' for module: $${dir}"; \
go mod tidy -go=1.18 -compat=1.18; \
done; \
git diff --exit-code;
.PHONY: mod-tidy
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 ./...; \
done;
.PHONY: vet
lint: ## Lint (using "golangci-lint")
golangci-lint run
.PHONY: lint
fmt: ## Format all Go files
gofmt -l -w -s .
.PHONY: fmt

View file

@ -1,8 +1,11 @@
<p align="center"> <p align="center">
<a href="https://sentry.io" target="_blank" align="center"> <a href="https://sentry.io/?utm_source=github&utm_medium=logo" target="_blank">
<img src="https://sentry-brand.storage.googleapis.com/sentry-logo-black.png" width="280"> <picture>
<source srcset="https://sentry-brand.storage.googleapis.com/sentry-logo-white.png" media="(prefers-color-scheme: dark)" />
<source srcset="https://sentry-brand.storage.googleapis.com/sentry-logo-black.png" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" />
<img src="https://sentry-brand.storage.googleapis.com/sentry-logo-black.png" alt="Sentry" width="280">
</picture>
</a> </a>
<br />
</p> </p>
# Official Sentry SDK for Go # Official Sentry SDK for Go
@ -14,11 +17,11 @@
[![go.dev](https://img.shields.io/badge/go.dev-pkg-007d9c.svg?style=flat)](https://pkg.go.dev/github.com/getsentry/sentry-go) [![go.dev](https://img.shields.io/badge/go.dev-pkg-007d9c.svg?style=flat)](https://pkg.go.dev/github.com/getsentry/sentry-go)
`sentry-go` provides a Sentry client implementation for the Go programming `sentry-go` provides a Sentry client implementation for the Go programming
language. This is the next line of the Go SDK for [Sentry](https://sentry.io/), language. This is the next generation of the Go SDK for [Sentry](https://sentry.io/),
intended to replace the `raven-go` package. intended to replace the `raven-go` package.
> Looking for the old `raven-go` SDK documentation? See the Legacy client section [here](https://docs.sentry.io/clients/go/). > Looking for the old `raven-go` SDK documentation? See the Legacy client section [here](https://docs.sentry.io/clients/go/).
> If you want to start using sentry-go instead, check out the [migration guide](https://docs.sentry.io/platforms/go/migration/). > If you want to start using `sentry-go` instead, check out the [migration guide](https://docs.sentry.io/platforms/go/migration/).
## Requirements ## Requirements
@ -26,7 +29,7 @@ The only requirement is a Go compiler.
We verify this package against the 3 most recent releases of Go. Those are the We verify this package against the 3 most recent releases of Go. Those are the
supported versions. The exact versions are defined in supported versions. The exact versions are defined in
[`GitHub workflow`](.github/workflows/ci.yml). [`GitHub workflow`](.github/workflows/test.yml).
In addition, we run tests against the current master branch of the Go toolchain, In addition, we run tests against the current master branch of the Go toolchain,
though support for this configuration is best-effort. though support for this configuration is best-effort.
@ -35,19 +38,11 @@ though support for this configuration is best-effort.
`sentry-go` can be installed like any other Go library through `go get`: `sentry-go` can be installed like any other Go library through `go get`:
```console
$ go get github.com/getsentry/sentry-go
```
Or, if you are already using
[Go Modules](https://github.com/golang/go/wiki/Modules), you may specify a
version number as well:
```console ```console
$ go get github.com/getsentry/sentry-go@latest $ go get github.com/getsentry/sentry-go@latest
``` ```
Check out the [list of released versions](https://pkg.go.dev/github.com/getsentry/sentry-go?tab=versions). Check out the [list of released versions](https://github.com/getsentry/sentry-go/releases).
## Configuration ## Configuration
@ -67,9 +62,9 @@ More on this in the [Configuration section of the official Sentry Go SDK documen
The SDK supports reporting errors and tracking application performance. The SDK supports reporting errors and tracking application performance.
To get started, have a look at one of our [examples](example/): To get started, have a look at one of our [examples](_examples/):
- [Basic error instrumentation](example/basic/main.go) - [Basic error instrumentation](_examples/basic/main.go)
- [Error and tracing for HTTP servers](example/http/main.go) - [Error and tracing for HTTP servers](_examples/http/main.go)
We also provide a [complete API reference](https://pkg.go.dev/github.com/getsentry/sentry-go). We also provide a [complete API reference](https://pkg.go.dev/github.com/getsentry/sentry-go).
@ -93,7 +88,7 @@ checkout the official documentation:
- [![GoDoc](https://godoc.org/github.com/getsentry/sentry-go?status.svg)](https://godoc.org/github.com/getsentry/sentry-go) - [![GoDoc](https://godoc.org/github.com/getsentry/sentry-go?status.svg)](https://godoc.org/github.com/getsentry/sentry-go)
- [![go.dev](https://img.shields.io/badge/go.dev-pkg-007d9c.svg?style=flat)](https://pkg.go.dev/github.com/getsentry/sentry-go) - [![go.dev](https://img.shields.io/badge/go.dev-pkg-007d9c.svg?style=flat)](https://pkg.go.dev/github.com/getsentry/sentry-go)
- [![Documentation](https://img.shields.io/badge/documentation-sentry.io-green.svg)](https://docs.sentry.io/platforms/go/) - [![Documentation](https://img.shields.io/badge/documentation-sentry.io-green.svg)](https://docs.sentry.io/platforms/go/)
- [![Forum](https://img.shields.io/badge/forum-sentry-green.svg)](https://forum.sentry.io/c/sdks) - [![Discussions](https://img.shields.io/github/discussions/getsentry/sentry-go.svg)](https://github.com/getsentry/sentry-go/discussions)
- [![Discord](https://img.shields.io/discord/621778831602221064)](https://discord.gg/Ww9hbqr) - [![Discord](https://img.shields.io/discord/621778831602221064)](https://discord.gg/Ww9hbqr)
- [![Stack Overflow](https://img.shields.io/badge/stack%20overflow-sentry-green.svg)](http://stackoverflow.com/questions/tagged/sentry) - [![Stack Overflow](https://img.shields.io/badge/stack%20overflow-sentry-green.svg)](http://stackoverflow.com/questions/tagged/sentry)
- [![Twitter Follow](https://img.shields.io/twitter/follow/getsentry?label=getsentry&style=social)](https://twitter.com/intent/follow?screen_name=getsentry) - [![Twitter Follow](https://img.shields.io/twitter/follow/getsentry?label=getsentry&style=social)](https://twitter.com/intent/follow?screen_name=getsentry)
@ -101,7 +96,7 @@ checkout the official documentation:
## License ## License
Licensed under Licensed under
[The 2-Clause BSD License](https://opensource.org/licenses/BSD-2-Clause), see [The MIT License](https://opensource.org/licenses/mit/), see
[`LICENSE`](LICENSE). [`LICENSE`](LICENSE).
## Community ## Community

117
vendor/github.com/getsentry/sentry-go/check_in.go generated vendored Normal file
View file

@ -0,0 +1,117 @@
package sentry
import "time"
type CheckInStatus string
const (
CheckInStatusInProgress CheckInStatus = "in_progress"
CheckInStatusOK CheckInStatus = "ok"
CheckInStatusError CheckInStatus = "error"
)
type checkInScheduleType string
const (
checkInScheduleTypeCrontab checkInScheduleType = "crontab"
checkInScheduleTypeInterval checkInScheduleType = "interval"
)
type MonitorSchedule interface {
// scheduleType is a private method that must be implemented for monitor schedule
// implementation. It should never be called. This method is made for having
// specific private implementation of MonitorSchedule interface.
scheduleType() checkInScheduleType
}
type crontabSchedule struct {
Type string `json:"type"`
Value string `json:"value"`
}
func (c crontabSchedule) scheduleType() checkInScheduleType {
return checkInScheduleTypeCrontab
}
// CrontabSchedule defines the MonitorSchedule with a cron format.
// Example: "8 * * * *".
func CrontabSchedule(scheduleString string) MonitorSchedule {
return crontabSchedule{
Type: string(checkInScheduleTypeCrontab),
Value: scheduleString,
}
}
type intervalSchedule struct {
Type string `json:"type"`
Value int64 `json:"value"`
Unit string `json:"unit"`
}
func (i intervalSchedule) scheduleType() checkInScheduleType {
return checkInScheduleTypeInterval
}
type MonitorScheduleUnit string
const (
MonitorScheduleUnitMinute MonitorScheduleUnit = "minute"
MonitorScheduleUnitHour MonitorScheduleUnit = "hour"
MonitorScheduleUnitDay MonitorScheduleUnit = "day"
MonitorScheduleUnitWeek MonitorScheduleUnit = "week"
MonitorScheduleUnitMonth MonitorScheduleUnit = "month"
MonitorScheduleUnitYear MonitorScheduleUnit = "year"
)
// IntervalSchedule defines the MonitorSchedule with an interval format.
//
// Example:
//
// IntervalSchedule(1, sentry.MonitorScheduleUnitDay)
func IntervalSchedule(value int64, unit MonitorScheduleUnit) MonitorSchedule {
return intervalSchedule{
Type: string(checkInScheduleTypeInterval),
Value: value,
Unit: string(unit),
}
}
type MonitorConfig struct { //nolint: maligned // prefer readability over optimal memory layout
Schedule MonitorSchedule `json:"schedule,omitempty"`
// The allowed margin of minutes after the expected check-in time that
// the monitor will not be considered missed for.
CheckInMargin int64 `json:"checkin_margin,omitempty"`
// The allowed duration in minutes that the monitor may be `in_progress`
// for before being considered failed due to timeout.
MaxRuntime int64 `json:"max_runtime,omitempty"`
// A tz database string representing the timezone which the monitor's execution schedule is in.
// See: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
Timezone string `json:"timezone,omitempty"`
}
type CheckIn struct { //nolint: maligned // prefer readability over optimal memory layout
// Check-In ID (unique and client generated)
ID EventID `json:"check_in_id"`
// The distinct slug of the monitor.
MonitorSlug string `json:"monitor_slug"`
// The status of the check-in.
Status CheckInStatus `json:"status"`
// The duration of the check-in. Will only take effect if the status is ok or error.
Duration time.Duration `json:"duration,omitempty"`
}
// serializedCheckIn is used by checkInMarshalJSON method on Event struct.
// See https://develop.sentry.dev/sdk/check-ins/
type serializedCheckIn struct { //nolint: maligned
// Check-In ID (unique and client generated).
CheckInID string `json:"check_in_id"`
// The distinct slug of the monitor.
MonitorSlug string `json:"monitor_slug"`
// The status of the check-in.
Status CheckInStatus `json:"status"`
// The duration of the check-in in seconds. Will only take effect if the status is ok or error.
Duration float64 `json:"duration,omitempty"`
Release string `json:"release,omitempty"`
Environment string `json:"environment,omitempty"`
MonitorConfig *MonitorConfig `json:"monitor_config,omitempty"`
}

View file

@ -3,15 +3,12 @@ package sentry
import ( import (
"context" "context"
"crypto/x509" "crypto/x509"
"errors"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"log" "log"
"math/rand" "math/rand"
"net/http" "net/http"
"os" "os"
"reflect"
"sort" "sort"
"strings" "strings"
"sync" "sync"
@ -20,6 +17,9 @@ import (
"github.com/getsentry/sentry-go/internal/debug" "github.com/getsentry/sentry-go/internal/debug"
) )
// The identifier of the SDK.
const sdkIdentifier = "sentry.go"
// maxErrorDepth is the maximum number of errors reported in a chain of errors. // maxErrorDepth is the maximum number of errors reported in a chain of errors.
// This protects the SDK from an arbitrarily long chain of wrapped errors. // This protects the SDK from an arbitrarily long chain of wrapped errors.
// //
@ -29,6 +29,11 @@ import (
// stack trace is often the most useful information. // stack trace is often the most useful information.
const maxErrorDepth = 10 const maxErrorDepth = 10
// defaultMaxSpans limits the default number of recorded spans per transaction. The limit is
// meant to bound memory usage and prevent too large transaction events that
// would be rejected by Sentry.
const defaultMaxSpans = 1000
// hostname is the host name reported by the kernel. It is precomputed once to // hostname is the host name reported by the kernel. It is precomputed once to
// avoid syscalls when capturing events. // avoid syscalls when capturing events.
// //
@ -60,7 +65,7 @@ func (r *lockedRand) Float64() float64 {
// other hand, the source returned from rand.NewSource is not safe for // other hand, the source returned from rand.NewSource is not safe for
// concurrent use, so we need to couple its use with a sync.Mutex. // concurrent use, so we need to couple its use with a sync.Mutex.
var rng = &lockedRand{ var rng = &lockedRand{
//#nosec G404 -- We are fine using transparent, non-secure value here. // #nosec G404 -- We are fine using transparent, non-secure value here.
r: rand.New(rand.NewSource(time.Now().UnixNano())), r: rand.New(rand.NewSource(time.Now().UnixNano())),
} }
@ -74,7 +79,7 @@ type usageError struct {
// Logger is an instance of log.Logger that is use to provide debug information about running Sentry Client // Logger is an instance of log.Logger that is use to provide debug information about running Sentry Client
// can be enabled by either using Logger.SetOutput directly or with Debug client option. // can be enabled by either using Logger.SetOutput directly or with Debug client option.
var Logger = log.New(ioutil.Discard, "[Sentry] ", log.LstdFlags) var Logger = log.New(io.Discard, "[Sentry] ", log.LstdFlags)
// EventProcessor is a function that processes an event. // EventProcessor is a function that processes an event.
// Event processors are used to change an event before it is sent to Sentry. // Event processors are used to change an event before it is sent to Sentry.
@ -122,18 +127,31 @@ type ClientOptions struct {
// 0.0 is treated as if it was 1.0. To drop all events, set the DSN to the // 0.0 is treated as if it was 1.0. To drop all events, set the DSN to the
// empty string. // empty string.
SampleRate float64 SampleRate float64
// Enable performance tracing.
EnableTracing bool
// The sample rate for sampling traces in the range [0.0, 1.0]. // The sample rate for sampling traces in the range [0.0, 1.0].
TracesSampleRate float64 TracesSampleRate float64
// Used to customize the sampling of traces, overrides TracesSampleRate. // Used to customize the sampling of traces, overrides TracesSampleRate.
TracesSampler TracesSampler TracesSampler TracesSampler
// The sample rate for profiling traces in the range [0.0, 1.0].
// This is relative to TracesSampleRate - it is a ratio of profiled traces out of all sampled traces.
ProfilesSampleRate float64
// List of regexp strings that will be used to match against event's message // List of regexp strings that will be used to match against event's message
// and if applicable, caught errors type and value. // and if applicable, caught errors type and value.
// If the match is found, then a whole event will be dropped. // If the match is found, then a whole event will be dropped.
IgnoreErrors []string IgnoreErrors []string
// List of regexp strings that will be used to match against a transaction's
// name. If a match is found, then the transaction will be dropped.
IgnoreTransactions []string
// If this flag is enabled, certain personally identifiable information (PII) is added by active integrations.
// By default, no such data is sent.
SendDefaultPII bool
// BeforeSend is called before error events are sent to Sentry. // BeforeSend is called before error events are sent to Sentry.
// Use it to mutate the event or return nil to discard the event. // Use it to mutate the event or return nil to discard the event.
// See EventProcessor if you need to mutate transactions.
BeforeSend func(event *Event, hint *EventHint) *Event BeforeSend func(event *Event, hint *EventHint) *Event
// BeforeSendTransaction is called before transaction events are sent to Sentry.
// Use it to mutate the transaction or return nil to discard the transaction.
BeforeSendTransaction func(event *Event, hint *EventHint) *Event
// Before breadcrumb add callback. // Before breadcrumb add callback.
BeforeBreadcrumb func(breadcrumb *Breadcrumb, hint *BreadcrumbHint) *Breadcrumb BeforeBreadcrumb func(breadcrumb *Breadcrumb, hint *BreadcrumbHint) *Breadcrumb
// Integrations to be installed on the current Client, receives default // Integrations to be installed on the current Client, receives default
@ -173,8 +191,14 @@ type ClientOptions struct {
Dist string Dist string
// The environment to be sent with events. // The environment to be sent with events.
Environment string Environment string
// Maximum number of breadcrumbs. // Maximum number of breadcrumbs
// when MaxBreadcrumbs is negative then ignore breadcrumbs.
MaxBreadcrumbs int MaxBreadcrumbs int
// Maximum number of spans.
//
// See https://develop.sentry.dev/sdk/envelopes/#size-limits for size limits
// applied during event ingestion. Events that exceed these limits might get dropped.
MaxSpans int
// An optional pointer to http.Client that will be used with a default // An optional pointer to http.Client that will be used with a default
// HTTPTransport. Using your own client will make HTTPTransport, HTTPProxy, // HTTPTransport. Using your own client will make HTTPTransport, HTTPProxy,
// HTTPSProxy and CaCerts options ignored. // HTTPSProxy and CaCerts options ignored.
@ -192,15 +216,28 @@ type ClientOptions struct {
HTTPSProxy string HTTPSProxy string
// An optional set of SSL certificates to use. // An optional set of SSL certificates to use.
CaCerts *x509.CertPool CaCerts *x509.CertPool
// MaxErrorDepth is the maximum number of errors reported in a chain of errors.
// This protects the SDK from an arbitrarily long chain of wrapped errors.
//
// An additional consideration is that arguably reporting a long chain of errors
// is of little use when debugging production errors with Sentry. The Sentry UI
// is not optimized for long chains either. The top-level error together with a
// stack trace is often the most useful information.
MaxErrorDepth int
// Default event tags. These are overridden by tags set on a scope.
Tags map[string]string
} }
// Client is the underlying processor that is used by the main API and Hub // Client is the underlying processor that is used by the main API and Hub
// instances. It must be created with NewClient. // instances. It must be created with NewClient.
type Client struct { type Client struct {
mu sync.RWMutex
options ClientOptions options ClientOptions
dsn *Dsn dsn *Dsn
eventProcessors []EventProcessor eventProcessors []EventProcessor
integrations []Integration integrations []Integration
sdkIdentifier string
sdkVersion string
// Transport is read-only. Replacing the transport of an existing client is // Transport is read-only. Replacing the transport of an existing client is
// not supported, create a new client instead. // not supported, create a new client instead.
Transport Transport Transport Transport
@ -214,8 +251,26 @@ type Client struct {
// single goroutine) or hub methods (for concurrent programs, for example web // single goroutine) or hub methods (for concurrent programs, for example web
// servers). // servers).
func NewClient(options ClientOptions) (*Client, error) { func NewClient(options ClientOptions) (*Client, error) {
if options.TracesSampleRate != 0.0 && options.TracesSampler != nil { // The default error event sample rate for all SDKs is 1.0 (send all).
return nil, errors.New("TracesSampleRate and TracesSampler are mutually exclusive") //
// In Go, the zero value (default) for float64 is 0.0, which means that
// constructing a client with NewClient(ClientOptions{}), or, equivalently,
// initializing the SDK with Init(ClientOptions{}) without an explicit
// SampleRate would drop all events.
//
// To retain the desired default behavior, we exceptionally flip SampleRate
// from 0.0 to 1.0 here. Setting the sample rate to 0.0 is not very useful
// anyway, and the same end result can be achieved in many other ways like
// not initializing the SDK, setting the DSN to the empty string or using an
// event processor that always returns nil.
//
// An alternative API could be such that default options don't need to be
// the same as Go's zero values, for example using the Functional Options
// pattern. That would either require a breaking change if we want to reuse
// the obvious NewClient name, or a new function as an alternative
// constructor.
if options.SampleRate == 0.0 {
options.SampleRate = 1.0
} }
if options.Debug { if options.Debug {
@ -238,6 +293,14 @@ func NewClient(options ClientOptions) (*Client, error) {
options.Environment = os.Getenv("SENTRY_ENVIRONMENT") options.Environment = os.Getenv("SENTRY_ENVIRONMENT")
} }
if options.MaxErrorDepth == 0 {
options.MaxErrorDepth = maxErrorDepth
}
if options.MaxSpans == 0 {
options.MaxSpans = defaultMaxSpans
}
// SENTRYGODEBUG is a comma-separated list of key=value pairs (similar // SENTRYGODEBUG is a comma-separated list of key=value pairs (similar
// to GODEBUG). It is not a supported feature: recognized debug options // to GODEBUG). It is not a supported feature: recognized debug options
// may change any time. // may change any time.
@ -271,8 +334,10 @@ func NewClient(options ClientOptions) (*Client, error) {
} }
client := Client{ client := Client{
options: options, options: options,
dsn: dsn, dsn: dsn,
sdkIdentifier: sdkIdentifier,
sdkVersion: SDKVersion,
} }
client.setupTransport() client.setupTransport()
@ -294,7 +359,7 @@ func (client *Client) setupTransport() {
// accommodate more concurrent events. // accommodate more concurrent events.
// TODO(tracing): consider using separate buffers per // TODO(tracing): consider using separate buffers per
// event type. // event type.
if opts.TracesSampleRate != 0 || opts.TracesSampler != nil { if opts.EnableTracing {
httpTransport.BufferSize = 1000 httpTransport.BufferSize = 1000
} }
transport = httpTransport transport = httpTransport
@ -311,6 +376,8 @@ func (client *Client) setupIntegrations() {
new(environmentIntegration), new(environmentIntegration),
new(modulesIntegration), new(modulesIntegration),
new(ignoreErrorsIntegration), new(ignoreErrorsIntegration),
new(ignoreTransactionsIntegration),
new(globalTagsIntegration),
} }
if client.options.Integrations != nil { if client.options.Integrations != nil {
@ -326,6 +393,10 @@ func (client *Client) setupIntegrations() {
integration.SetupOnce(client) integration.SetupOnce(client)
Logger.Printf("Integration installed: %s\n", integration.Name()) Logger.Printf("Integration installed: %s\n", integration.Name())
} }
sort.Slice(client.integrations, func(i, j int) bool {
return client.integrations[i].Name() < client.integrations[j].Name()
})
} }
// AddEventProcessor adds an event processor to the client. It must not be // AddEventProcessor adds an event processor to the client. It must not be
@ -340,22 +411,33 @@ func (client *Client) AddEventProcessor(processor EventProcessor) {
} }
// Options return ClientOptions for the current Client. // Options return ClientOptions for the current Client.
func (client Client) Options() ClientOptions { func (client *Client) Options() ClientOptions {
// Note: internally, consider using `client.options` instead of `client.Options()` to avoid copying the object each time.
return client.options return client.options
} }
// CaptureMessage captures an arbitrary message. // CaptureMessage captures an arbitrary message.
func (client *Client) CaptureMessage(message string, hint *EventHint, scope EventModifier) *EventID { func (client *Client) CaptureMessage(message string, hint *EventHint, scope EventModifier) *EventID {
event := client.eventFromMessage(message, LevelInfo) event := client.EventFromMessage(message, LevelInfo)
return client.CaptureEvent(event, hint, scope) return client.CaptureEvent(event, hint, scope)
} }
// CaptureException captures an error. // CaptureException captures an error.
func (client *Client) CaptureException(exception error, hint *EventHint, scope EventModifier) *EventID { func (client *Client) CaptureException(exception error, hint *EventHint, scope EventModifier) *EventID {
event := client.eventFromException(exception, LevelError) event := client.EventFromException(exception, LevelError)
return client.CaptureEvent(event, hint, scope) return client.CaptureEvent(event, hint, scope)
} }
// CaptureCheckIn captures a check in.
func (client *Client) CaptureCheckIn(checkIn *CheckIn, monitorConfig *MonitorConfig, scope EventModifier) *EventID {
event := client.EventFromCheckIn(checkIn, monitorConfig)
if event != nil && event.CheckIn != nil {
client.CaptureEvent(event, nil, scope)
return &event.CheckIn.ID
}
return nil
}
// CaptureEvent captures an event on the currently active client if any. // CaptureEvent captures an event on the currently active client if any.
// //
// The event must already be assembled. Typically code would instead use // The event must already be assembled. Typically code would instead use
@ -376,7 +458,7 @@ func (client *Client) Recover(err interface{}, hint *EventHint, scope EventModif
// use the Context for communicating deadline nor cancelation. All it does // use the Context for communicating deadline nor cancelation. All it does
// is store the Context in the EventHint and there nil means the Context is // is store the Context in the EventHint and there nil means the Context is
// not available. // not available.
//nolint: staticcheck // nolint: staticcheck
return client.RecoverWithContext(nil, err, hint, scope) return client.RecoverWithContext(nil, err, hint, scope)
} }
@ -407,11 +489,11 @@ func (client *Client) RecoverWithContext(
var event *Event var event *Event
switch err := err.(type) { switch err := err.(type) {
case error: case error:
event = client.eventFromException(err, LevelFatal) event = client.EventFromException(err, LevelFatal)
case string: case string:
event = client.eventFromMessage(err, LevelFatal) event = client.EventFromMessage(err, LevelFatal)
default: default:
event = client.eventFromMessage(fmt.Sprintf("%#v", err), LevelFatal) event = client.EventFromMessage(fmt.Sprintf("%#v", err), LevelFatal)
} }
return client.CaptureEvent(event, hint, scope) return client.CaptureEvent(event, hint, scope)
} }
@ -431,16 +513,17 @@ func (client *Client) Flush(timeout time.Duration) bool {
return client.Transport.Flush(timeout) return client.Transport.Flush(timeout)
} }
func (client *Client) eventFromMessage(message string, level Level) *Event { // EventFromMessage creates an event from the given message string.
func (client *Client) EventFromMessage(message string, level Level) *Event {
if message == "" { if message == "" {
err := usageError{fmt.Errorf("%s called with empty message", callerFunctionName())} err := usageError{fmt.Errorf("%s called with empty message", callerFunctionName())}
return client.eventFromException(err, level) return client.EventFromException(err, level)
} }
event := NewEvent() event := NewEvent()
event.Level = level event.Level = level
event.Message = message event.Message = message
if client.Options().AttachStacktrace { if client.options.AttachStacktrace {
event.Threads = []Thread{{ event.Threads = []Thread{{
Stacktrace: NewStacktrace(), Stacktrace: NewStacktrace(),
Crashed: false, Crashed: false,
@ -451,45 +534,62 @@ func (client *Client) eventFromMessage(message string, level Level) *Event {
return event return event
} }
func (client *Client) eventFromException(exception error, level Level) *Event { // EventFromException creates a new Sentry event from the given `error` instance.
func (client *Client) EventFromException(exception error, level Level) *Event {
event := NewEvent()
event.Level = level
err := exception err := exception
if err == nil { if err == nil {
err = usageError{fmt.Errorf("%s called with nil error", callerFunctionName())} err = usageError{fmt.Errorf("%s called with nil error", callerFunctionName())}
} }
event := NewEvent() event.SetException(err, client.options.MaxErrorDepth)
event.Level = level
for i := 0; i < maxErrorDepth && err != nil; i++ {
event.Exception = append(event.Exception, Exception{
Value: err.Error(),
Type: reflect.TypeOf(err).String(),
Stacktrace: ExtractStacktrace(err),
})
switch previous := err.(type) {
case interface{ Unwrap() error }:
err = previous.Unwrap()
case interface{ Cause() error }:
err = previous.Cause()
default:
err = nil
}
}
// Add a trace of the current stack to the most recent error in a chain if
// it doesn't have a stack trace yet.
// We only add to the most recent error to avoid duplication and because the
// current stack is most likely unrelated to errors deeper in the chain.
if event.Exception[0].Stacktrace == nil {
event.Exception[0].Stacktrace = NewStacktrace()
}
// event.Exception should be sorted such that the most recent error is last.
reverse(event.Exception)
return event return event
} }
// EventFromCheckIn creates a new Sentry event from the given `check_in` instance.
func (client *Client) EventFromCheckIn(checkIn *CheckIn, monitorConfig *MonitorConfig) *Event {
if checkIn == nil {
return nil
}
event := NewEvent()
event.Type = checkInType
var checkInID EventID
if checkIn.ID == "" {
checkInID = EventID(uuid())
} else {
checkInID = checkIn.ID
}
event.CheckIn = &CheckIn{
ID: checkInID,
MonitorSlug: checkIn.MonitorSlug,
Status: checkIn.Status,
Duration: checkIn.Duration,
}
event.MonitorConfig = monitorConfig
return event
}
func (client *Client) SetSDKIdentifier(identifier string) {
client.mu.Lock()
defer client.mu.Unlock()
client.sdkIdentifier = identifier
}
func (client *Client) GetSDKIdentifier() string {
client.mu.RLock()
defer client.mu.RUnlock()
return client.sdkIdentifier
}
// reverse reverses the slice a in place. // reverse reverses the slice a in place.
func reverse(a []Exception) { func reverse(a []Exception) {
for i := len(a)/2 - 1; i >= 0; i-- { for i := len(a)/2 - 1; i >= 0; i-- {
@ -504,34 +604,10 @@ func (client *Client) processEvent(event *Event, hint *EventHint, scope EventMod
return client.CaptureException(err, hint, scope) return client.CaptureException(err, hint, scope)
} }
options := client.Options()
// The default error event sample rate for all SDKs is 1.0 (send all).
//
// In Go, the zero value (default) for float64 is 0.0, which means that
// constructing a client with NewClient(ClientOptions{}), or, equivalently,
// initializing the SDK with Init(ClientOptions{}) without an explicit
// SampleRate would drop all events.
//
// To retain the desired default behavior, we exceptionally flip SampleRate
// from 0.0 to 1.0 here. Setting the sample rate to 0.0 is not very useful
// anyway, and the same end result can be achieved in many other ways like
// not initializing the SDK, setting the DSN to the empty string or using an
// event processor that always returns nil.
//
// An alternative API could be such that default options don't need to be
// the same as Go's zero values, for example using the Functional Options
// pattern. That would either require a breaking change if we want to reuse
// the obvious NewClient name, or a new function as an alternative
// constructor.
if options.SampleRate == 0.0 {
options.SampleRate = 1.0
}
// Transactions are sampled by options.TracesSampleRate or // Transactions are sampled by options.TracesSampleRate or
// options.TracesSampler when they are started. All other events // options.TracesSampler when they are started. Other events
// (errors, messages) are sampled here. // (errors, messages) are sampled here. Does not apply to check-ins.
if event.Type != transactionType && !sample(options.SampleRate) { if event.Type != transactionType && event.Type != checkInType && !sample(client.options.SampleRate) {
Logger.Println("Event dropped due to SampleRate hit.") Logger.Println("Event dropped due to SampleRate hit.")
return nil return nil
} }
@ -540,12 +616,19 @@ func (client *Client) processEvent(event *Event, hint *EventHint, scope EventMod
return nil return nil
} }
// As per spec, transactions do not go through BeforeSend. // Apply beforeSend* processors
if event.Type != transactionType && options.BeforeSend != nil { if hint == nil {
if hint == nil { hint = &EventHint{}
hint = &EventHint{} }
if event.Type == transactionType && client.options.BeforeSendTransaction != nil {
// Transaction events
if event = client.options.BeforeSendTransaction(event, hint); event == nil {
Logger.Println("Transaction dropped due to BeforeSendTransaction callback.")
return nil
} }
if event = options.BeforeSend(event, hint); event == nil { } else if event.Type != transactionType && event.Type != checkInType && client.options.BeforeSend != nil {
// All other events
if event = client.options.BeforeSend(event, hint); event == nil {
Logger.Println("Event dropped due to BeforeSend callback.") Logger.Println("Event dropped due to BeforeSend callback.")
return nil return nil
} }
@ -558,6 +641,7 @@ func (client *Client) processEvent(event *Event, hint *EventHint, scope EventMod
func (client *Client) prepareEvent(event *Event, hint *EventHint, scope EventModifier) *Event { func (client *Client) prepareEvent(event *Event, hint *EventHint, scope EventModifier) *Event {
if event.EventID == "" { if event.EventID == "" {
// TODO set EventID when the event is created, same as in other SDKs. It's necessary for profileTransaction.ID.
event.EventID = EventID(uuid()) event.EventID = EventID(uuid())
} }
@ -570,33 +654,33 @@ func (client *Client) prepareEvent(event *Event, hint *EventHint, scope EventMod
} }
if event.ServerName == "" { if event.ServerName == "" {
if client.Options().ServerName != "" { event.ServerName = client.options.ServerName
event.ServerName = client.Options().ServerName
} else { if event.ServerName == "" {
event.ServerName = hostname event.ServerName = hostname
} }
} }
if event.Release == "" && client.Options().Release != "" { if event.Release == "" {
event.Release = client.Options().Release event.Release = client.options.Release
} }
if event.Dist == "" && client.Options().Dist != "" { if event.Dist == "" {
event.Dist = client.Options().Dist event.Dist = client.options.Dist
} }
if event.Environment == "" && client.Options().Environment != "" { if event.Environment == "" {
event.Environment = client.Options().Environment event.Environment = client.options.Environment
} }
event.Platform = "go" event.Platform = "go"
event.Sdk = SdkInfo{ event.Sdk = SdkInfo{
Name: "sentry.go", Name: client.GetSDKIdentifier(),
Version: Version, Version: SDKVersion,
Integrations: client.listIntegrations(), Integrations: client.listIntegrations(),
Packages: []SdkPackage{{ Packages: []SdkPackage{{
Name: "sentry-go", Name: "sentry-go",
Version: Version, Version: SDKVersion,
}}, }},
} }
@ -625,19 +709,22 @@ func (client *Client) prepareEvent(event *Event, hint *EventHint, scope EventMod
} }
} }
if event.sdkMetaData.transactionProfile != nil {
event.sdkMetaData.transactionProfile.UpdateFromEvent(event)
}
return event return event
} }
func (client Client) listIntegrations() []string { func (client *Client) listIntegrations() []string {
integrations := make([]string, 0, len(client.integrations)) integrations := make([]string, len(client.integrations))
for _, integration := range client.integrations { for i, integration := range client.integrations {
integrations = append(integrations, integration.Name()) integrations[i] = integration.Name()
} }
sort.Strings(integrations)
return integrations return integrations
} }
func (client Client) integrationAlreadyInstalled(name string) bool { func (client *Client) integrationAlreadyInstalled(name string) bool {
for _, integration := range client.integrations { for _, integration := range client.integrations {
if integration.Name() == name { if integration.Name() == name {
return true return true

View file

@ -1,63 +1,6 @@
/* /*
Package sentry is the official Sentry SDK for Go. Package repository: https://github.com/getsentry/sentry-go/
Use it to report errors and track application performance through distributed For more information about Sentry and SDK features, please have a look at the official documentation site: https://docs.sentry.io/platforms/go/
tracing.
For more information about Sentry and SDK features please have a look at the
documentation site https://docs.sentry.io/platforms/go/.
Basic Usage
The first step is to initialize the SDK, providing at a minimum the DSN of your
Sentry project. This step is accomplished through a call to sentry.Init.
func main() {
err := sentry.Init(...)
...
}
A more detailed yet simple example is available at
https://github.com/getsentry/sentry-go/blob/master/example/basic/main.go.
Error Reporting
The Capture* functions report messages and errors to Sentry.
sentry.CaptureMessage(...)
sentry.CaptureException(...)
sentry.CaptureEvent(...)
Use similarly named functions in the Hub for concurrent programs like web
servers.
Performance Monitoring
You can use Sentry to monitor your application's performance. More information
on the product page https://docs.sentry.io/product/performance/.
The StartSpan function creates new spans.
span := sentry.StartSpan(ctx, "operation")
...
span.Finish()
Integrations
The SDK has support for several Go frameworks, available as subpackages.
Getting Support
For paid Sentry.io accounts, head out to https://sentry.io/support.
For all users, support channels include:
Forum: https://forum.sentry.io
Discord: https://discord.gg/Ww9hbqr (#go channel)
If you found an issue with the SDK, please report through
https://github.com/getsentry/sentry-go/issues/new/choose.
For responsibly disclosing a security issue, please follow the steps in
https://sentry.io/security/#vulnerability-disclosure.
*/ */
package sentry package sentry

View file

@ -145,19 +145,44 @@ func (dsn Dsn) String() string {
return url return url
} }
// StoreAPIURL returns the URL of the store endpoint of the project associated // Get the scheme of the DSN.
// with the DSN. func (dsn Dsn) GetScheme() string {
func (dsn Dsn) StoreAPIURL() *url.URL { return string(dsn.scheme)
return dsn.getAPIURL("store")
} }
// EnvelopeAPIURL returns the URL of the envelope endpoint of the project // Get the public key of the DSN.
func (dsn Dsn) GetPublicKey() string {
return dsn.publicKey
}
// Get the secret key of the DSN.
func (dsn Dsn) GetSecretKey() string {
return dsn.secretKey
}
// Get the host of the DSN.
func (dsn Dsn) GetHost() string {
return dsn.host
}
// Get the port of the DSN.
func (dsn Dsn) GetPort() int {
return dsn.port
}
// Get the path of the DSN.
func (dsn Dsn) GetPath() string {
return dsn.path
}
// Get the project ID of the DSN.
func (dsn Dsn) GetProjectID() string {
return dsn.projectID
}
// GetAPIURL returns the URL of the envelope endpoint of the project
// associated with the DSN. // associated with the DSN.
func (dsn Dsn) EnvelopeAPIURL() *url.URL { func (dsn Dsn) GetAPIURL() *url.URL {
return dsn.getAPIURL("envelope")
}
func (dsn Dsn) getAPIURL(s string) *url.URL {
var rawURL string var rawURL string
rawURL += fmt.Sprintf("%s://%s", dsn.scheme, dsn.host) rawURL += fmt.Sprintf("%s://%s", dsn.scheme, dsn.host)
if dsn.port != dsn.scheme.defaultPort() { if dsn.port != dsn.scheme.defaultPort() {
@ -166,15 +191,20 @@ func (dsn Dsn) getAPIURL(s string) *url.URL {
if dsn.path != "" { if dsn.path != "" {
rawURL += dsn.path rawURL += dsn.path
} }
rawURL += fmt.Sprintf("/api/%s/%s/", dsn.projectID, s) rawURL += fmt.Sprintf("/api/%s/%s/", dsn.projectID, "envelope")
parsedURL, _ := url.Parse(rawURL) parsedURL, _ := url.Parse(rawURL)
return parsedURL return parsedURL
} }
// RequestHeaders returns all the necessary headers that have to be used in the transport. // RequestHeaders returns all the necessary headers that have to be used in the transport when seinding events
// to the /store endpoint.
//
// Deprecated: This method shall only be used if you want to implement your own transport that sends events to
// the /store endpoint. If you're using the transport provided by the SDK, all necessary headers to authenticate
// against the /envelope endpoint are added automatically.
func (dsn Dsn) RequestHeaders() map[string]string { func (dsn Dsn) RequestHeaders() map[string]string {
auth := fmt.Sprintf("Sentry sentry_version=%s, sentry_timestamp=%d, "+ auth := fmt.Sprintf("Sentry sentry_version=%s, sentry_timestamp=%d, "+
"sentry_client=sentry.go/%s, sentry_key=%s", apiVersion, time.Now().Unix(), Version, dsn.publicKey) "sentry_client=sentry.go/%s, sentry_key=%s", apiVersion, time.Now().Unix(), SDKVersion, dsn.publicKey)
if dsn.secretKey != "" { if dsn.secretKey != "" {
auth = fmt.Sprintf("%s, sentry_secret=%s", auth, dsn.secretKey) auth = fmt.Sprintf("%s, sentry_secret=%s", auth, dsn.secretKey)

View file

@ -0,0 +1,123 @@
package sentry
import (
"strconv"
"strings"
"github.com/getsentry/sentry-go/internal/otel/baggage"
)
const (
sentryPrefix = "sentry-"
)
// DynamicSamplingContext holds information about the current event that can be used to make dynamic sampling decisions.
type DynamicSamplingContext struct {
Entries map[string]string
Frozen bool
}
func DynamicSamplingContextFromHeader(header []byte) (DynamicSamplingContext, error) {
bag, err := baggage.Parse(string(header))
if err != nil {
return DynamicSamplingContext{}, err
}
entries := map[string]string{}
for _, member := range bag.Members() {
// We only store baggage members if their key starts with "sentry-".
if k, v := member.Key(), member.Value(); strings.HasPrefix(k, sentryPrefix) {
entries[strings.TrimPrefix(k, sentryPrefix)] = v
}
}
return DynamicSamplingContext{
Entries: entries,
// If there's at least one Sentry value, we consider the DSC frozen
Frozen: len(entries) > 0,
}, nil
}
func DynamicSamplingContextFromTransaction(span *Span) DynamicSamplingContext {
entries := map[string]string{}
hub := hubFromContext(span.Context())
scope := hub.Scope()
client := hub.Client()
if client == nil || scope == nil {
return DynamicSamplingContext{
Entries: map[string]string{},
Frozen: false,
}
}
if traceID := span.TraceID.String(); traceID != "" {
entries["trace_id"] = traceID
}
if sampleRate := span.sampleRate; sampleRate != 0 {
entries["sample_rate"] = strconv.FormatFloat(sampleRate, 'f', -1, 64)
}
if dsn := client.dsn; dsn != nil {
if publicKey := dsn.publicKey; publicKey != "" {
entries["public_key"] = publicKey
}
}
if release := client.options.Release; release != "" {
entries["release"] = release
}
if environment := client.options.Environment; environment != "" {
entries["environment"] = environment
}
// Only include the transaction name if it's of good quality (not empty and not SourceURL)
if span.Source != "" && span.Source != SourceURL {
if span.IsTransaction() {
entries["transaction"] = span.Name
}
}
if userSegment := scope.user.Segment; userSegment != "" {
entries["user_segment"] = userSegment
}
if span.Sampled.Bool() {
entries["sampled"] = "true"
} else {
entries["sampled"] = "false"
}
return DynamicSamplingContext{
Entries: entries,
Frozen: true,
}
}
func (d DynamicSamplingContext) HasEntries() bool {
return len(d.Entries) > 0
}
func (d DynamicSamplingContext) IsFrozen() bool {
return d.Frozen
}
func (d DynamicSamplingContext) String() string {
members := []baggage.Member{}
for k, entry := range d.Entries {
member, err := baggage.NewMember(sentryPrefix+k, entry)
if err != nil {
continue
}
members = append(members, member)
}
if len(members) > 0 {
baggage, err := baggage.New(members...)
if err != nil {
return ""
}
return baggage.String()
}
return ""
}

View file

@ -267,6 +267,18 @@ func (hub *Hub) CaptureException(exception error) *EventID {
return eventID return eventID
} }
// CaptureCheckIn calls the method of the same name on currently bound Client instance
// passing it a top-level Scope.
// Returns CheckInID if the check-in was captured successfully, or nil otherwise.
func (hub *Hub) CaptureCheckIn(checkIn *CheckIn, monitorConfig *MonitorConfig) *EventID {
client, scope := hub.Client(), hub.Scope()
if client == nil {
return nil
}
return client.CaptureCheckIn(checkIn, monitorConfig, scope)
}
// AddBreadcrumb records a new breadcrumb. // AddBreadcrumb records a new breadcrumb.
// //
// The total number of breadcrumbs that can be recorded are limited by the // The total number of breadcrumbs that can be recorded are limited by the
@ -280,31 +292,27 @@ func (hub *Hub) AddBreadcrumb(breadcrumb *Breadcrumb, hint *BreadcrumbHint) {
return return
} }
options := client.Options() max := client.options.MaxBreadcrumbs
max := defaultMaxBreadcrumbs
if options.MaxBreadcrumbs != 0 {
max = options.MaxBreadcrumbs
}
if max < 0 { if max < 0 {
return return
} }
if options.BeforeBreadcrumb != nil { if client.options.BeforeBreadcrumb != nil {
h := &BreadcrumbHint{} if hint == nil {
if hint != nil { hint = &BreadcrumbHint{}
h = hint
} }
if breadcrumb = options.BeforeBreadcrumb(breadcrumb, h); breadcrumb == nil { if breadcrumb = client.options.BeforeBreadcrumb(breadcrumb, hint); breadcrumb == nil {
Logger.Println("breadcrumb dropped due to BeforeBreadcrumb callback.") Logger.Println("breadcrumb dropped due to BeforeBreadcrumb callback.")
return return
} }
} }
if max > maxBreadcrumbs { if max == 0 {
max = defaultMaxBreadcrumbs
} else if max > maxBreadcrumbs {
max = maxBreadcrumbs max = maxBreadcrumbs
} }
hub.Scope().AddBreadcrumb(breadcrumb, max) hub.Scope().AddBreadcrumb(breadcrumb, max)
} }

View file

@ -2,6 +2,7 @@ package sentry
import ( import (
"fmt" "fmt"
"os"
"regexp" "regexp"
"runtime" "runtime"
"runtime/debug" "runtime/debug"
@ -26,7 +27,7 @@ func (mi *modulesIntegration) SetupOnce(client *Client) {
client.AddEventProcessor(mi.processor) client.AddEventProcessor(mi.processor)
} }
func (mi *modulesIntegration) processor(event *Event, hint *EventHint) *Event { func (mi *modulesIntegration) processor(event *Event, _ *EventHint) *Event {
if len(event.Modules) == 0 { if len(event.Modules) == 0 {
mi.once.Do(func() { mi.once.Do(func() {
info, ok := debug.ReadBuildInfo() info, ok := debug.ReadBuildInfo()
@ -69,21 +70,22 @@ func (ei *environmentIntegration) SetupOnce(client *Client) {
client.AddEventProcessor(ei.processor) client.AddEventProcessor(ei.processor)
} }
func (ei *environmentIntegration) processor(event *Event, hint *EventHint) *Event { func (ei *environmentIntegration) processor(event *Event, _ *EventHint) *Event {
// Initialize maps as necessary. // Initialize maps as necessary.
contextNames := []string{"device", "os", "runtime"}
if event.Contexts == nil { if event.Contexts == nil {
event.Contexts = make(map[string]interface{}) event.Contexts = make(map[string]Context, len(contextNames))
} }
for _, name := range []string{"device", "os", "runtime"} { for _, name := range contextNames {
if event.Contexts[name] == nil { if event.Contexts[name] == nil {
event.Contexts[name] = make(map[string]interface{}) event.Contexts[name] = make(Context)
} }
} }
// Set contextual information preserving existing data. For each context, if // Set contextual information preserving existing data. For each context, if
// the existing value is not of type map[string]interface{}, then no // the existing value is not of type map[string]interface{}, then no
// additional information is added. // additional information is added.
if deviceContext, ok := event.Contexts["device"].(map[string]interface{}); ok { if deviceContext, ok := event.Contexts["device"]; ok {
if _, ok := deviceContext["arch"]; !ok { if _, ok := deviceContext["arch"]; !ok {
deviceContext["arch"] = runtime.GOARCH deviceContext["arch"] = runtime.GOARCH
} }
@ -91,12 +93,12 @@ func (ei *environmentIntegration) processor(event *Event, hint *EventHint) *Even
deviceContext["num_cpu"] = runtime.NumCPU() deviceContext["num_cpu"] = runtime.NumCPU()
} }
} }
if osContext, ok := event.Contexts["os"].(map[string]interface{}); ok { if osContext, ok := event.Contexts["os"]; ok {
if _, ok := osContext["name"]; !ok { if _, ok := osContext["name"]; !ok {
osContext["name"] = runtime.GOOS osContext["name"] = runtime.GOOS
} }
} }
if runtimeContext, ok := event.Contexts["runtime"].(map[string]interface{}); ok { if runtimeContext, ok := event.Contexts["runtime"]; ok {
if _, ok := runtimeContext["name"]; !ok { if _, ok := runtimeContext["name"]; !ok {
runtimeContext["name"] = "go" runtimeContext["name"] = "go"
} }
@ -129,11 +131,11 @@ func (iei *ignoreErrorsIntegration) Name() string {
} }
func (iei *ignoreErrorsIntegration) SetupOnce(client *Client) { func (iei *ignoreErrorsIntegration) SetupOnce(client *Client) {
iei.ignoreErrors = transformStringsIntoRegexps(client.Options().IgnoreErrors) iei.ignoreErrors = transformStringsIntoRegexps(client.options.IgnoreErrors)
client.AddEventProcessor(iei.processor) client.AddEventProcessor(iei.processor)
} }
func (iei *ignoreErrorsIntegration) processor(event *Event, hint *EventHint) *Event { func (iei *ignoreErrorsIntegration) processor(event *Event, _ *EventHint) *Event {
suspects := getIgnoreErrorsSuspects(event) suspects := getIgnoreErrorsSuspects(event)
for _, suspect := range suspects { for _, suspect := range suspects {
@ -176,6 +178,40 @@ func getIgnoreErrorsSuspects(event *Event) []string {
return suspects return suspects
} }
// ================================
// Ignore Transactions Integration
// ================================
type ignoreTransactionsIntegration struct {
ignoreTransactions []*regexp.Regexp
}
func (iei *ignoreTransactionsIntegration) Name() string {
return "IgnoreTransactions"
}
func (iei *ignoreTransactionsIntegration) SetupOnce(client *Client) {
iei.ignoreTransactions = transformStringsIntoRegexps(client.options.IgnoreTransactions)
client.AddEventProcessor(iei.processor)
}
func (iei *ignoreTransactionsIntegration) processor(event *Event, _ *EventHint) *Event {
suspect := event.Transaction
if suspect == "" {
return event
}
for _, pattern := range iei.ignoreTransactions {
if pattern.Match([]byte(suspect)) {
Logger.Printf("Transaction dropped due to being matched by `IgnoreTransactions` option."+
"| Value matched: %s | Filter used: %s", suspect, pattern)
return nil
}
}
return event
}
// ================================ // ================================
// Contextify Frames Integration // Contextify Frames Integration
// ================================ // ================================
@ -197,7 +233,7 @@ func (cfi *contextifyFramesIntegration) SetupOnce(client *Client) {
client.AddEventProcessor(cfi.processor) client.AddEventProcessor(cfi.processor)
} }
func (cfi *contextifyFramesIntegration) processor(event *Event, hint *EventHint) *Event { func (cfi *contextifyFramesIntegration) processor(event *Event, _ *EventHint) *Event {
// Range over all exceptions // Range over all exceptions
for _, ex := range event.Exception { for _, ex := range event.Exception {
// If it has no stacktrace, just bail out // If it has no stacktrace, just bail out
@ -290,3 +326,66 @@ func (cfi *contextifyFramesIntegration) addContextLinesToFrame(frame Frame, line
} }
return frame return frame
} }
// ================================
// Global Tags Integration
// ================================
const envTagsPrefix = "SENTRY_TAGS_"
type globalTagsIntegration struct {
tags map[string]string
envTags map[string]string
}
func (ti *globalTagsIntegration) Name() string {
return "GlobalTags"
}
func (ti *globalTagsIntegration) SetupOnce(client *Client) {
ti.tags = make(map[string]string, len(client.options.Tags))
for k, v := range client.options.Tags {
ti.tags[k] = v
}
ti.envTags = loadEnvTags()
client.AddEventProcessor(ti.processor)
}
func (ti *globalTagsIntegration) processor(event *Event, _ *EventHint) *Event {
if len(ti.tags) == 0 && len(ti.envTags) == 0 {
return event
}
if event.Tags == nil {
event.Tags = make(map[string]string, len(ti.tags)+len(ti.envTags))
}
for k, v := range ti.tags {
if _, ok := event.Tags[k]; !ok {
event.Tags[k] = v
}
}
for k, v := range ti.envTags {
if _, ok := event.Tags[k]; !ok {
event.Tags[k] = v
}
}
return event
}
func loadEnvTags() map[string]string {
tags := map[string]string{}
for _, pair := range os.Environ() {
parts := strings.Split(pair, "=")
if !strings.HasPrefix(parts[0], envTagsPrefix) {
continue
}
tag := strings.TrimPrefix(parts[0], envTagsPrefix)
tags[tag] = parts[1]
}
return tags
}

View file

@ -6,16 +6,24 @@ import (
"fmt" "fmt"
"net" "net"
"net/http" "net/http"
"reflect"
"strings" "strings"
"time" "time"
) )
// Protocol Docs (kinda) // eventType is the type of an error event.
// https://github.com/getsentry/rust-sentry-types/blob/master/src/protocol/v7.rs const eventType = "event"
// transactionType is the type of a transaction event. // transactionType is the type of a transaction event.
const transactionType = "transaction" const transactionType = "transaction"
// profileType is the type of a profile event.
// currently, profiles are always sent as part of a transaction event.
const profileType = "profile"
// checkInType is the type of a check in event.
const checkInType = "check_in"
// Level marks the severity of the event. // Level marks the severity of the event.
type Level string type Level string
@ -28,6 +36,15 @@ const (
LevelFatal Level = "fatal" LevelFatal Level = "fatal"
) )
func getSensitiveHeaders() map[string]bool {
return map[string]bool{
"Authorization": true,
"Cookie": true,
"X-Forwarded-For": true,
"X-Real-Ip": true,
}
}
// SdkInfo contains all metadata about about the SDK being used. // SdkInfo contains all metadata about about the SDK being used.
type SdkInfo struct { type SdkInfo struct {
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
@ -90,13 +107,56 @@ func (b *Breadcrumb) MarshalJSON() ([]byte, error) {
return json.Marshal((*breadcrumb)(b)) return json.Marshal((*breadcrumb)(b))
} }
// Attachment allows associating files with your events to aid in investigation.
// An event may contain one or more attachments.
type Attachment struct {
Filename string
ContentType string
Payload []byte
}
// User describes the user associated with an Event. If this is used, at least // User describes the user associated with an Event. If this is used, at least
// an ID or an IP address should be provided. // an ID or an IP address should be provided.
type User struct { type User struct {
Email string `json:"email,omitempty"` ID string `json:"id,omitempty"`
ID string `json:"id,omitempty"` Email string `json:"email,omitempty"`
IPAddress string `json:"ip_address,omitempty"` IPAddress string `json:"ip_address,omitempty"`
Username string `json:"username,omitempty"` Username string `json:"username,omitempty"`
Name string `json:"name,omitempty"`
Segment string `json:"segment,omitempty"`
Data map[string]string `json:"data,omitempty"`
}
func (u User) IsEmpty() bool {
if len(u.ID) > 0 {
return false
}
if len(u.Email) > 0 {
return false
}
if len(u.IPAddress) > 0 {
return false
}
if len(u.Username) > 0 {
return false
}
if len(u.Name) > 0 {
return false
}
if len(u.Segment) > 0 {
return false
}
if len(u.Data) > 0 {
return false
}
return true
} }
// Request contains information on a HTTP request related to the event. // Request contains information on a HTTP request related to the event.
@ -121,23 +181,35 @@ func NewRequest(r *http.Request) *Request {
} }
url := fmt.Sprintf("%s://%s%s", protocol, r.Host, r.URL.Path) url := fmt.Sprintf("%s://%s%s", protocol, r.Host, r.URL.Path)
// We read only the first Cookie header because of the specification: var cookies string
// https://tools.ietf.org/html/rfc6265#section-5.4
// When the user agent generates an HTTP request, the user agent MUST NOT
// attach more than one Cookie header field.
cookies := r.Header.Get("Cookie")
headers := make(map[string]string, len(r.Header))
for k, v := range r.Header {
headers[k] = strings.Join(v, ",")
}
headers["Host"] = r.Host
var env map[string]string var env map[string]string
if addr, port, err := net.SplitHostPort(r.RemoteAddr); err == nil { headers := map[string]string{}
env = map[string]string{"REMOTE_ADDR": addr, "REMOTE_PORT": port}
if client := CurrentHub().Client(); client != nil && client.options.SendDefaultPII {
// We read only the first Cookie header because of the specification:
// https://tools.ietf.org/html/rfc6265#section-5.4
// When the user agent generates an HTTP request, the user agent MUST NOT
// attach more than one Cookie header field.
cookies = r.Header.Get("Cookie")
for k, v := range r.Header {
headers[k] = strings.Join(v, ",")
}
if addr, port, err := net.SplitHostPort(r.RemoteAddr); err == nil {
env = map[string]string{"REMOTE_ADDR": addr, "REMOTE_PORT": port}
}
} else {
sensitiveHeaders := getSensitiveHeaders()
for k, v := range r.Header {
if _, ok := sensitiveHeaders[k]; !ok {
headers[k] = strings.Join(v, ",")
}
}
} }
headers["Host"] = r.Host
return &Request{ return &Request{
URL: url, URL: url,
Method: r.Method, Method: r.Method,
@ -148,23 +220,82 @@ func NewRequest(r *http.Request) *Request {
} }
} }
// Mechanism is the mechanism by which an exception was generated and handled.
type Mechanism struct {
Type string `json:"type,omitempty"`
Description string `json:"description,omitempty"`
HelpLink string `json:"help_link,omitempty"`
Handled *bool `json:"handled,omitempty"`
Data map[string]interface{} `json:"data,omitempty"`
}
// SetUnhandled indicates that the exception is an unhandled exception, i.e.
// from a panic.
func (m *Mechanism) SetUnhandled() {
h := false
m.Handled = &h
}
// Exception specifies an error that occurred. // Exception specifies an error that occurred.
type Exception struct { type Exception struct {
Type string `json:"type,omitempty"` // used as the main issue title Type string `json:"type,omitempty"` // used as the main issue title
Value string `json:"value,omitempty"` // used as the main issue subtitle Value string `json:"value,omitempty"` // used as the main issue subtitle
Module string `json:"module,omitempty"` Module string `json:"module,omitempty"`
ThreadID string `json:"thread_id,omitempty"` ThreadID uint64 `json:"thread_id,omitempty"`
Stacktrace *Stacktrace `json:"stacktrace,omitempty"` Stacktrace *Stacktrace `json:"stacktrace,omitempty"`
Mechanism *Mechanism `json:"mechanism,omitempty"`
}
// SDKMetaData is a struct to stash data which is needed at some point in the SDK's event processing pipeline
// but which shouldn't get send to Sentry.
type SDKMetaData struct {
dsc DynamicSamplingContext
transactionProfile *profileInfo
}
// Contains information about how the name of the transaction was determined.
type TransactionInfo struct {
Source TransactionSource `json:"source,omitempty"`
}
// The DebugMeta interface is not used in Golang apps, but may be populated
// when proxying Events from other platforms, like iOS, Android, and the
// Web. (See: https://develop.sentry.dev/sdk/event-payloads/debugmeta/ ).
type DebugMeta struct {
SdkInfo *DebugMetaSdkInfo `json:"sdk_info,omitempty"`
Images []DebugMetaImage `json:"images,omitempty"`
}
type DebugMetaSdkInfo struct {
SdkName string `json:"sdk_name,omitempty"`
VersionMajor int `json:"version_major,omitempty"`
VersionMinor int `json:"version_minor,omitempty"`
VersionPatchlevel int `json:"version_patchlevel,omitempty"`
}
type DebugMetaImage struct {
Type string `json:"type,omitempty"` // all
ImageAddr string `json:"image_addr,omitempty"` // macho,elf,pe
ImageSize int `json:"image_size,omitempty"` // macho,elf,pe
DebugID string `json:"debug_id,omitempty"` // macho,elf,pe,wasm,sourcemap
DebugFile string `json:"debug_file,omitempty"` // macho,elf,pe,wasm
CodeID string `json:"code_id,omitempty"` // macho,elf,pe,wasm
CodeFile string `json:"code_file,omitempty"` // macho,elf,pe,wasm,sourcemap
ImageVmaddr string `json:"image_vmaddr,omitempty"` // macho,elf,pe
Arch string `json:"arch,omitempty"` // macho,elf,pe
UUID string `json:"uuid,omitempty"` // proguard
} }
// EventID is a hexadecimal string representing a unique uuid4 for an Event. // EventID is a hexadecimal string representing a unique uuid4 for an Event.
// An EventID must be 32 characters long, lowercase and not have any dashes. // An EventID must be 32 characters long, lowercase and not have any dashes.
type EventID string type EventID string
type Context = map[string]interface{}
// Event is the fundamental data structure that is sent to Sentry. // Event is the fundamental data structure that is sent to Sentry.
type Event struct { type Event struct {
Breadcrumbs []*Breadcrumb `json:"breadcrumbs,omitempty"` Breadcrumbs []*Breadcrumb `json:"breadcrumbs,omitempty"`
Contexts map[string]interface{} `json:"contexts,omitempty"` Contexts map[string]Context `json:"contexts,omitempty"`
Dist string `json:"dist,omitempty"` Dist string `json:"dist,omitempty"`
Environment string `json:"environment,omitempty"` Environment string `json:"environment,omitempty"`
EventID EventID `json:"event_id,omitempty"` EventID EventID `json:"event_id,omitempty"`
@ -185,12 +316,62 @@ type Event struct {
Modules map[string]string `json:"modules,omitempty"` Modules map[string]string `json:"modules,omitempty"`
Request *Request `json:"request,omitempty"` Request *Request `json:"request,omitempty"`
Exception []Exception `json:"exception,omitempty"` Exception []Exception `json:"exception,omitempty"`
DebugMeta *DebugMeta `json:"debug_meta,omitempty"`
Attachments []*Attachment `json:"-"`
// The fields below are only relevant for transactions. // The fields below are only relevant for transactions.
Type string `json:"type,omitempty"` Type string `json:"type,omitempty"`
StartTime time.Time `json:"start_timestamp"` StartTime time.Time `json:"start_timestamp"`
Spans []*Span `json:"spans,omitempty"` Spans []*Span `json:"spans,omitempty"`
TransactionInfo *TransactionInfo `json:"transaction_info,omitempty"`
// The fields below are only relevant for crons/check ins
CheckIn *CheckIn `json:"check_in,omitempty"`
MonitorConfig *MonitorConfig `json:"monitor_config,omitempty"`
// The fields below are not part of the final JSON payload.
sdkMetaData SDKMetaData
}
// SetException appends the unwrapped errors to the event's exception list.
//
// maxErrorDepth is the maximum depth of the error chain we will look
// into while unwrapping the errors.
func (e *Event) SetException(exception error, maxErrorDepth int) {
err := exception
if err == nil {
return
}
for i := 0; i < maxErrorDepth && err != nil; i++ {
e.Exception = append(e.Exception, Exception{
Value: err.Error(),
Type: reflect.TypeOf(err).String(),
Stacktrace: ExtractStacktrace(err),
})
switch previous := err.(type) {
case interface{ Unwrap() error }:
err = previous.Unwrap()
case interface{ Cause() error }:
err = previous.Cause()
default:
err = nil
}
}
// Add a trace of the current stack to the most recent error in a chain if
// it doesn't have a stack trace yet.
// We only add to the most recent error to avoid duplication and because the
// current stack is most likely unrelated to errors deeper in the chain.
if e.Exception[0].Stacktrace == nil {
e.Exception[0].Stacktrace = NewStacktrace()
}
// event.Exception should be sorted such that the most recent error is last.
reverse(e.Exception)
} }
// TODO: Event.Contexts map[string]interface{} => map[string]EventContext, // TODO: Event.Contexts map[string]interface{} => map[string]EventContext,
@ -210,6 +391,8 @@ func (e *Event) MarshalJSON() ([]byte, error) {
// and a few type tricks. // and a few type tricks.
if e.Type == transactionType { if e.Type == transactionType {
return e.transactionMarshalJSON() return e.transactionMarshalJSON()
} else if e.Type == checkInType {
return e.checkInMarshalJSON()
} }
return e.defaultMarshalJSON() return e.defaultMarshalJSON()
} }
@ -232,9 +415,10 @@ func (e *Event) defaultMarshalJSON() ([]byte, error) {
// be sent for transactions. They shadow the respective fields in Event // be sent for transactions. They shadow the respective fields in Event
// and are meant to remain nil, triggering the omitempty behavior. // and are meant to remain nil, triggering the omitempty behavior.
Type json.RawMessage `json:"type,omitempty"` Type json.RawMessage `json:"type,omitempty"`
StartTime json.RawMessage `json:"start_timestamp,omitempty"` StartTime json.RawMessage `json:"start_timestamp,omitempty"`
Spans json.RawMessage `json:"spans,omitempty"` Spans json.RawMessage `json:"spans,omitempty"`
TransactionInfo json.RawMessage `json:"transaction_info,omitempty"`
} }
x := errorEvent{event: (*event)(e)} x := errorEvent{event: (*event)(e)}
@ -283,10 +467,33 @@ func (e *Event) transactionMarshalJSON() ([]byte, error) {
return json.Marshal(x) return json.Marshal(x)
} }
func (e *Event) checkInMarshalJSON() ([]byte, error) {
checkIn := serializedCheckIn{
CheckInID: string(e.CheckIn.ID),
MonitorSlug: e.CheckIn.MonitorSlug,
Status: e.CheckIn.Status,
Duration: e.CheckIn.Duration.Seconds(),
Release: e.Release,
Environment: e.Environment,
MonitorConfig: nil,
}
if e.MonitorConfig != nil {
checkIn.MonitorConfig = &MonitorConfig{
Schedule: e.MonitorConfig.Schedule,
CheckInMargin: e.MonitorConfig.CheckInMargin,
MaxRuntime: e.MonitorConfig.MaxRuntime,
Timezone: e.MonitorConfig.Timezone,
}
}
return json.Marshal(checkIn)
}
// NewEvent creates a new Event. // NewEvent creates a new Event.
func NewEvent() *Event { func NewEvent() *Event {
event := Event{ event := Event{
Contexts: make(map[string]interface{}), Contexts: make(map[string]Context),
Extra: make(map[string]interface{}), Extra: make(map[string]interface{}),
Tags: make(map[string]string), Tags: make(map[string]string),
Modules: make(map[string]string), Modules: make(map[string]string),

View file

@ -1,23 +0,0 @@
package randutil
import (
"crypto/rand"
"encoding/binary"
)
const (
floatMax = 1 << 53
floatMask = floatMax - 1
)
// Float64 returns a cryptographically secure random number in [0.0, 1.0).
func Float64() float64 {
// The implementation is, in essence:
// return float64(rand.Int63n(1<<53)) / (1<<53)
b := make([]byte, 8)
_, err := rand.Read(b)
if err != nil {
panic(err)
}
return float64(binary.LittleEndian.Uint64(b)&floatMask) / floatMax
}

View file

@ -0,0 +1,12 @@
## Why do we have this "otel/baggage" folder?
The root sentry-go SDK (namely, the Dynamic Sampling functionality) needs an implementation of the [baggage spec](https://www.w3.org/TR/baggage/).
For that reason, we've taken the existing baggage implementation from the [opentelemetry-go](https://github.com/open-telemetry/opentelemetry-go/) repository, and fixed a few things that in our opinion were violating the specification.
These issues are:
1. Baggage string value `one%20two` should be properly parsed as "one two"
1. Baggage string value `one+two` should be parsed as "one+two"
1. Go string value "one two" should be encoded as `one%20two` (percent encoding), and NOT as `one+two` (URL query encoding).
1. Go string value "1=1" might be encoded as `1=1`, because the spec says: "Note, value MAY contain any number of the equal sign (=) characters. Parsers MUST NOT assume that the equal sign is only used to separate key and value.". `1%3D1` is also valid, but to simplify the implementation we're not doing it.
Changes were made in this PR: https://github.com/getsentry/sentry-go/pull/568

View file

@ -0,0 +1,604 @@
// Adapted from https://github.com/open-telemetry/opentelemetry-go/blob/c21b6b6bb31a2f74edd06e262f1690f3f6ea3d5c/baggage/baggage.go
//
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package baggage
import (
"errors"
"fmt"
"net/url"
"regexp"
"strings"
"unicode/utf8"
"github.com/getsentry/sentry-go/internal/otel/baggage/internal/baggage"
)
const (
maxMembers = 180
maxBytesPerMembers = 4096
maxBytesPerBaggageString = 8192
listDelimiter = ","
keyValueDelimiter = "="
propertyDelimiter = ";"
keyDef = `([\x21\x23-\x27\x2A\x2B\x2D\x2E\x30-\x39\x41-\x5a\x5e-\x7a\x7c\x7e]+)`
valueDef = `([\x21\x23-\x2b\x2d-\x3a\x3c-\x5B\x5D-\x7e]*)`
keyValueDef = `\s*` + keyDef + `\s*` + keyValueDelimiter + `\s*` + valueDef + `\s*`
)
var (
keyRe = regexp.MustCompile(`^` + keyDef + `$`)
valueRe = regexp.MustCompile(`^` + valueDef + `$`)
propertyRe = regexp.MustCompile(`^(?:\s*` + keyDef + `\s*|` + keyValueDef + `)$`)
)
var (
errInvalidKey = errors.New("invalid key")
errInvalidValue = errors.New("invalid value")
errInvalidProperty = errors.New("invalid baggage list-member property")
errInvalidMember = errors.New("invalid baggage list-member")
errMemberNumber = errors.New("too many list-members in baggage-string")
errMemberBytes = errors.New("list-member too large")
errBaggageBytes = errors.New("baggage-string too large")
)
// Property is an additional metadata entry for a baggage list-member.
type Property struct {
key, value string
// hasValue indicates if a zero-value value means the property does not
// have a value or if it was the zero-value.
hasValue bool
// hasData indicates whether the created property contains data or not.
// Properties that do not contain data are invalid with no other check
// required.
hasData bool
}
// NewKeyProperty returns a new Property for key.
//
// If key is invalid, an error will be returned.
func NewKeyProperty(key string) (Property, error) {
if !keyRe.MatchString(key) {
return newInvalidProperty(), fmt.Errorf("%w: %q", errInvalidKey, key)
}
p := Property{key: key, hasData: true}
return p, nil
}
// NewKeyValueProperty returns a new Property for key with value.
//
// If key or value are invalid, an error will be returned.
func NewKeyValueProperty(key, value string) (Property, error) {
if !keyRe.MatchString(key) {
return newInvalidProperty(), fmt.Errorf("%w: %q", errInvalidKey, key)
}
if !valueRe.MatchString(value) {
return newInvalidProperty(), fmt.Errorf("%w: %q", errInvalidValue, value)
}
p := Property{
key: key,
value: value,
hasValue: true,
hasData: true,
}
return p, nil
}
func newInvalidProperty() Property {
return Property{}
}
// parseProperty attempts to decode a Property from the passed string. It
// returns an error if the input is invalid according to the W3C Baggage
// specification.
func parseProperty(property string) (Property, error) {
if property == "" {
return newInvalidProperty(), nil
}
match := propertyRe.FindStringSubmatch(property)
if len(match) != 4 {
return newInvalidProperty(), fmt.Errorf("%w: %q", errInvalidProperty, property)
}
p := Property{hasData: true}
if match[1] != "" {
p.key = match[1]
} else {
p.key = match[2]
p.value = match[3]
p.hasValue = true
}
return p, nil
}
// validate ensures p conforms to the W3C Baggage specification, returning an
// error otherwise.
func (p Property) validate() error {
errFunc := func(err error) error {
return fmt.Errorf("invalid property: %w", err)
}
if !p.hasData {
return errFunc(fmt.Errorf("%w: %q", errInvalidProperty, p))
}
if !keyRe.MatchString(p.key) {
return errFunc(fmt.Errorf("%w: %q", errInvalidKey, p.key))
}
if p.hasValue && !valueRe.MatchString(p.value) {
return errFunc(fmt.Errorf("%w: %q", errInvalidValue, p.value))
}
if !p.hasValue && p.value != "" {
return errFunc(errors.New("inconsistent value"))
}
return nil
}
// Key returns the Property key.
func (p Property) Key() string {
return p.key
}
// Value returns the Property value. Additionally, a boolean value is returned
// indicating if the returned value is the empty if the Property has a value
// that is empty or if the value is not set.
func (p Property) Value() (string, bool) {
return p.value, p.hasValue
}
// String encodes Property into a string compliant with the W3C Baggage
// specification.
func (p Property) String() string {
if p.hasValue {
return fmt.Sprintf("%s%s%v", p.key, keyValueDelimiter, p.value)
}
return p.key
}
type properties []Property
func fromInternalProperties(iProps []baggage.Property) properties {
if len(iProps) == 0 {
return nil
}
props := make(properties, len(iProps))
for i, p := range iProps {
props[i] = Property{
key: p.Key,
value: p.Value,
hasValue: p.HasValue,
}
}
return props
}
func (p properties) asInternal() []baggage.Property {
if len(p) == 0 {
return nil
}
iProps := make([]baggage.Property, len(p))
for i, prop := range p {
iProps[i] = baggage.Property{
Key: prop.key,
Value: prop.value,
HasValue: prop.hasValue,
}
}
return iProps
}
func (p properties) Copy() properties {
if len(p) == 0 {
return nil
}
props := make(properties, len(p))
copy(props, p)
return props
}
// validate ensures each Property in p conforms to the W3C Baggage
// specification, returning an error otherwise.
func (p properties) validate() error {
for _, prop := range p {
if err := prop.validate(); err != nil {
return err
}
}
return nil
}
// String encodes properties into a string compliant with the W3C Baggage
// specification.
func (p properties) String() string {
props := make([]string, len(p))
for i, prop := range p {
props[i] = prop.String()
}
return strings.Join(props, propertyDelimiter)
}
// Member is a list-member of a baggage-string as defined by the W3C Baggage
// specification.
type Member struct {
key, value string
properties properties
// hasData indicates whether the created property contains data or not.
// Properties that do not contain data are invalid with no other check
// required.
hasData bool
}
// NewMember returns a new Member from the passed arguments. The key will be
// used directly while the value will be url decoded after validation. An error
// is returned if the created Member would be invalid according to the W3C
// Baggage specification.
func NewMember(key, value string, props ...Property) (Member, error) {
m := Member{
key: key,
value: value,
properties: properties(props).Copy(),
hasData: true,
}
if err := m.validate(); err != nil {
return newInvalidMember(), err
}
//// NOTE(anton): I don't think we need to unescape here
// decodedValue, err := url.PathUnescape(value)
// if err != nil {
// return newInvalidMember(), fmt.Errorf("%w: %q", errInvalidValue, value)
// }
// m.value = decodedValue
return m, nil
}
func newInvalidMember() Member {
return Member{}
}
// parseMember attempts to decode a Member from the passed string. It returns
// an error if the input is invalid according to the W3C Baggage
// specification.
func parseMember(member string) (Member, error) {
if n := len(member); n > maxBytesPerMembers {
return newInvalidMember(), fmt.Errorf("%w: %d", errMemberBytes, n)
}
var (
key, value string
props properties
)
parts := strings.SplitN(member, propertyDelimiter, 2)
switch len(parts) {
case 2:
// Parse the member properties.
for _, pStr := range strings.Split(parts[1], propertyDelimiter) {
p, err := parseProperty(pStr)
if err != nil {
return newInvalidMember(), err
}
props = append(props, p)
}
fallthrough
case 1:
// Parse the member key/value pair.
// Take into account a value can contain equal signs (=).
kv := strings.SplitN(parts[0], keyValueDelimiter, 2)
if len(kv) != 2 {
return newInvalidMember(), fmt.Errorf("%w: %q", errInvalidMember, member)
}
// "Leading and trailing whitespaces are allowed but MUST be trimmed
// when converting the header into a data structure."
key = strings.TrimSpace(kv[0])
value = strings.TrimSpace(kv[1])
var err error
if !keyRe.MatchString(key) {
return newInvalidMember(), fmt.Errorf("%w: %q", errInvalidKey, key)
}
if !valueRe.MatchString(value) {
return newInvalidMember(), fmt.Errorf("%w: %q", errInvalidValue, value)
}
decodedValue, err := url.PathUnescape(value)
if err != nil {
return newInvalidMember(), fmt.Errorf("%w: %q", err, value)
}
value = decodedValue
default:
// This should never happen unless a developer has changed the string
// splitting somehow. Panic instead of failing silently and allowing
// the bug to slip past the CI checks.
panic("failed to parse baggage member")
}
return Member{key: key, value: value, properties: props, hasData: true}, nil
}
// validate ensures m conforms to the W3C Baggage specification.
// A key is just an ASCII string, but a value must be URL encoded UTF-8,
// returning an error otherwise.
func (m Member) validate() error {
if !m.hasData {
return fmt.Errorf("%w: %q", errInvalidMember, m)
}
if !keyRe.MatchString(m.key) {
return fmt.Errorf("%w: %q", errInvalidKey, m.key)
}
//// NOTE(anton): IMO it's too early to validate the value here.
// if !valueRe.MatchString(m.value) {
// return fmt.Errorf("%w: %q", errInvalidValue, m.value)
// }
return m.properties.validate()
}
// Key returns the Member key.
func (m Member) Key() string { return m.key }
// Value returns the Member value.
func (m Member) Value() string { return m.value }
// Properties returns a copy of the Member properties.
func (m Member) Properties() []Property { return m.properties.Copy() }
// String encodes Member into a string compliant with the W3C Baggage
// specification.
func (m Member) String() string {
// A key is just an ASCII string, but a value is URL encoded UTF-8.
s := fmt.Sprintf("%s%s%s", m.key, keyValueDelimiter, percentEncodeValue(m.value))
if len(m.properties) > 0 {
s = fmt.Sprintf("%s%s%s", s, propertyDelimiter, m.properties.String())
}
return s
}
// percentEncodeValue encodes the baggage value, using percent-encoding for
// disallowed octets.
func percentEncodeValue(s string) string {
const upperhex = "0123456789ABCDEF"
var sb strings.Builder
for byteIndex, width := 0, 0; byteIndex < len(s); byteIndex += width {
runeValue, w := utf8.DecodeRuneInString(s[byteIndex:])
width = w
char := string(runeValue)
if valueRe.MatchString(char) && char != "%" {
// The character is returned as is, no need to percent-encode
sb.WriteString(char)
} else {
// We need to percent-encode each byte of the multi-octet character
for j := 0; j < width; j++ {
b := s[byteIndex+j]
sb.WriteByte('%')
// Bitwise operations are inspired by "net/url"
sb.WriteByte(upperhex[b>>4])
sb.WriteByte(upperhex[b&15])
}
}
}
return sb.String()
}
// Baggage is a list of baggage members representing the baggage-string as
// defined by the W3C Baggage specification.
type Baggage struct { //nolint:golint
list baggage.List
}
// New returns a new valid Baggage. It returns an error if it results in a
// Baggage exceeding limits set in that specification.
//
// It expects all the provided members to have already been validated.
func New(members ...Member) (Baggage, error) {
if len(members) == 0 {
return Baggage{}, nil
}
b := make(baggage.List)
for _, m := range members {
if !m.hasData {
return Baggage{}, errInvalidMember
}
// OpenTelemetry resolves duplicates by last-one-wins.
b[m.key] = baggage.Item{
Value: m.value,
Properties: m.properties.asInternal(),
}
}
// Check member numbers after deduplication.
if len(b) > maxMembers {
return Baggage{}, errMemberNumber
}
bag := Baggage{b}
if n := len(bag.String()); n > maxBytesPerBaggageString {
return Baggage{}, fmt.Errorf("%w: %d", errBaggageBytes, n)
}
return bag, nil
}
// Parse attempts to decode a baggage-string from the passed string. It
// returns an error if the input is invalid according to the W3C Baggage
// specification.
//
// If there are duplicate list-members contained in baggage, the last one
// defined (reading left-to-right) will be the only one kept. This diverges
// from the W3C Baggage specification which allows duplicate list-members, but
// conforms to the OpenTelemetry Baggage specification.
func Parse(bStr string) (Baggage, error) {
if bStr == "" {
return Baggage{}, nil
}
if n := len(bStr); n > maxBytesPerBaggageString {
return Baggage{}, fmt.Errorf("%w: %d", errBaggageBytes, n)
}
b := make(baggage.List)
for _, memberStr := range strings.Split(bStr, listDelimiter) {
m, err := parseMember(memberStr)
if err != nil {
return Baggage{}, err
}
// OpenTelemetry resolves duplicates by last-one-wins.
b[m.key] = baggage.Item{
Value: m.value,
Properties: m.properties.asInternal(),
}
}
// OpenTelemetry does not allow for duplicate list-members, but the W3C
// specification does. Now that we have deduplicated, ensure the baggage
// does not exceed list-member limits.
if len(b) > maxMembers {
return Baggage{}, errMemberNumber
}
return Baggage{b}, nil
}
// Member returns the baggage list-member identified by key.
//
// If there is no list-member matching the passed key the returned Member will
// be a zero-value Member.
// The returned member is not validated, as we assume the validation happened
// when it was added to the Baggage.
func (b Baggage) Member(key string) Member {
v, ok := b.list[key]
if !ok {
// We do not need to worry about distinguishing between the situation
// where a zero-valued Member is included in the Baggage because a
// zero-valued Member is invalid according to the W3C Baggage
// specification (it has an empty key).
return newInvalidMember()
}
return Member{
key: key,
value: v.Value,
properties: fromInternalProperties(v.Properties),
hasData: true,
}
}
// Members returns all the baggage list-members.
// The order of the returned list-members does not have significance.
//
// The returned members are not validated, as we assume the validation happened
// when they were added to the Baggage.
func (b Baggage) Members() []Member {
if len(b.list) == 0 {
return nil
}
members := make([]Member, 0, len(b.list))
for k, v := range b.list {
members = append(members, Member{
key: k,
value: v.Value,
properties: fromInternalProperties(v.Properties),
hasData: true,
})
}
return members
}
// SetMember returns a copy the Baggage with the member included. If the
// baggage contains a Member with the same key the existing Member is
// replaced.
//
// If member is invalid according to the W3C Baggage specification, an error
// is returned with the original Baggage.
func (b Baggage) SetMember(member Member) (Baggage, error) {
if !member.hasData {
return b, errInvalidMember
}
n := len(b.list)
if _, ok := b.list[member.key]; !ok {
n++
}
list := make(baggage.List, n)
for k, v := range b.list {
// Do not copy if we are just going to overwrite.
if k == member.key {
continue
}
list[k] = v
}
list[member.key] = baggage.Item{
Value: member.value,
Properties: member.properties.asInternal(),
}
return Baggage{list: list}, nil
}
// DeleteMember returns a copy of the Baggage with the list-member identified
// by key removed.
func (b Baggage) DeleteMember(key string) Baggage {
n := len(b.list)
if _, ok := b.list[key]; ok {
n--
}
list := make(baggage.List, n)
for k, v := range b.list {
if k == key {
continue
}
list[k] = v
}
return Baggage{list: list}
}
// Len returns the number of list-members in the Baggage.
func (b Baggage) Len() int {
return len(b.list)
}
// String encodes Baggage into a string compliant with the W3C Baggage
// specification. The returned string will be invalid if the Baggage contains
// any invalid list-members.
func (b Baggage) String() string {
members := make([]string, 0, len(b.list))
for k, v := range b.list {
members = append(members, Member{
key: k,
value: v.Value,
properties: fromInternalProperties(v.Properties),
}.String())
}
return strings.Join(members, listDelimiter)
}

View file

@ -0,0 +1,45 @@
// Adapted from https://github.com/open-telemetry/opentelemetry-go/blob/c21b6b6bb31a2f74edd06e262f1690f3f6ea3d5c/internal/baggage/baggage.go
//
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/*
Package baggage provides base types and functionality to store and retrieve
baggage in Go context. This package exists because the OpenTracing bridge to
OpenTelemetry needs to synchronize state whenever baggage for a context is
modified and that context contains an OpenTracing span. If it were not for
this need this package would not need to exist and the
`go.opentelemetry.io/otel/baggage` package would be the singular place where
W3C baggage is handled.
*/
package baggage
// List is the collection of baggage members. The W3C allows for duplicates,
// but OpenTelemetry does not, therefore, this is represented as a map.
type List map[string]Item
// Item is the value and metadata properties part of a list-member.
type Item struct {
Value string
Properties []Property
}
// Property is a metadata entry for a list-member.
type Property struct {
Key, Value string
// HasValue indicates if a zero-value value means the property does not
// have a value or if it was the zero-value.
HasValue bool
}

View file

@ -1,6 +1,11 @@
package ratelimit package ratelimit
import "strings" import (
"strings"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
// Reference: // Reference:
// https://github.com/getsentry/relay/blob/0424a2e017d193a93918053c90cdae9472d164bf/relay-common/src/constants.rs#L116-L127 // https://github.com/getsentry/relay/blob/0424a2e017d193a93918053c90cdae9472d164bf/relay-common/src/constants.rs#L116-L127
@ -31,11 +36,11 @@ func (c Category) String() string {
case "": case "":
return "CategoryAll" return "CategoryAll"
default: default:
var b strings.Builder caser := cases.Title(language.English)
b.WriteString("Category") rv := "Category"
for _, w := range strings.Fields(string(c)) { for _, w := range strings.Fields(string(c)) {
b.WriteString(strings.Title(w)) rv += caser.String(w)
} }
return b.String() return rv
} }
} }

View file

@ -15,7 +15,7 @@ var errInvalidXSRLRetryAfter = errors.New("invalid retry-after value")
// //
// Example // Example
// //
// X-Sentry-Rate-Limits: 60:transaction, 2700:default;error;security // X-Sentry-Rate-Limits: 60:transaction, 2700:default;error;security
// //
// This will rate limit transactions for the next 60 seconds and errors for the // This will rate limit transactions for the next 60 seconds and errors for the
// next 2700 seconds. // next 2700 seconds.

View file

@ -0,0 +1,15 @@
## Benchmark results
```
goos: windows
goarch: amd64
pkg: github.com/getsentry/sentry-go/internal/trace
cpu: 12th Gen Intel(R) Core(TM) i7-12700K
BenchmarkEqualBytes-20 44323621 26.08 ns/op
BenchmarkStringEqual-20 60980257 18.27 ns/op
BenchmarkEqualPrefix-20 41369181 31.12 ns/op
BenchmarkFullParse-20 702012 1507 ns/op 1353.42 MB/s 1024 B/op 6 allocs/op
BenchmarkFramesIterator-20 1229971 969.3 ns/op 896 B/op 5 allocs/op
BenchmarkFramesReversedIterator-20 1271061 944.5 ns/op 896 B/op 5 allocs/op
BenchmarkSplitOnly-20 2250800 534.0 ns/op 3818.23 MB/s 128 B/op 1 allocs/op
```

View file

@ -0,0 +1,217 @@
package traceparser
import (
"bytes"
"strconv"
)
var blockSeparator = []byte("\n\n")
var lineSeparator = []byte("\n")
// Parses multi-stacktrace text dump produced by runtime.Stack([]byte, all=true).
// The parser prioritizes performance but requires the input to be well-formed in order to return correct data.
// See https://github.com/golang/go/blob/go1.20.4/src/runtime/mprof.go#L1191
func Parse(data []byte) TraceCollection {
var it = TraceCollection{}
if len(data) > 0 {
it.blocks = bytes.Split(data, blockSeparator)
}
return it
}
type TraceCollection struct {
blocks [][]byte
}
func (it TraceCollection) Length() int {
return len(it.blocks)
}
// Returns the stacktrace item at the given index.
func (it *TraceCollection) Item(i int) Trace {
// The first item may have a leading data separator and the last one may have a trailing one.
// Note: Trim() doesn't make a copy for single-character cutset under 0x80. It will just slice the original.
var data []byte
switch {
case i == 0:
data = bytes.TrimLeft(it.blocks[i], "\n")
case i == len(it.blocks)-1:
data = bytes.TrimRight(it.blocks[i], "\n")
default:
data = it.blocks[i]
}
var splitAt = bytes.IndexByte(data, '\n')
if splitAt < 0 {
return Trace{header: data}
}
return Trace{
header: data[:splitAt],
data: data[splitAt+1:],
}
}
// Trace represents a single stacktrace block, identified by a Goroutine ID and a sequence of Frames.
type Trace struct {
header []byte
data []byte
}
var goroutinePrefix = []byte("goroutine ")
// GoID parses the Goroutine ID from the header.
func (t *Trace) GoID() (id uint64) {
if bytes.HasPrefix(t.header, goroutinePrefix) {
var line = t.header[len(goroutinePrefix):]
var splitAt = bytes.IndexByte(line, ' ')
if splitAt >= 0 {
id, _ = strconv.ParseUint(string(line[:splitAt]), 10, 64)
}
}
return id
}
// UniqueIdentifier can be used as a map key to identify the trace.
func (t *Trace) UniqueIdentifier() []byte {
return t.data
}
func (t *Trace) Frames() FrameIterator {
var lines = bytes.Split(t.data, lineSeparator)
return FrameIterator{lines: lines, i: 0, len: len(lines)}
}
func (t *Trace) FramesReversed() ReverseFrameIterator {
var lines = bytes.Split(t.data, lineSeparator)
return ReverseFrameIterator{lines: lines, i: len(lines)}
}
const framesElided = "...additional frames elided..."
// FrameIterator iterates over stack frames.
type FrameIterator struct {
lines [][]byte
i int
len int
}
// Next returns the next frame, or nil if there are none.
func (it *FrameIterator) Next() Frame {
return Frame{it.popLine(), it.popLine()}
}
func (it *FrameIterator) popLine() []byte {
switch {
case it.i >= it.len:
return nil
case string(it.lines[it.i]) == framesElided:
it.i++
return it.popLine()
default:
it.i++
return it.lines[it.i-1]
}
}
// HasNext return true if there are values to be read.
func (it *FrameIterator) HasNext() bool {
return it.i < it.len
}
// LengthUpperBound returns the maximum number of elements this stacks may contain.
// The actual number may be lower because of elided frames. As such, the returned value
// cannot be used to iterate over the frames but may be used to reserve capacity.
func (it *FrameIterator) LengthUpperBound() int {
return it.len / 2
}
// ReverseFrameIterator iterates over stack frames in reverse order.
type ReverseFrameIterator struct {
lines [][]byte
i int
}
// Next returns the next frame, or nil if there are none.
func (it *ReverseFrameIterator) Next() Frame {
var line2 = it.popLine()
return Frame{it.popLine(), line2}
}
func (it *ReverseFrameIterator) popLine() []byte {
it.i--
switch {
case it.i < 0:
return nil
case string(it.lines[it.i]) == framesElided:
return it.popLine()
default:
return it.lines[it.i]
}
}
// HasNext return true if there are values to be read.
func (it *ReverseFrameIterator) HasNext() bool {
return it.i > 1
}
// LengthUpperBound returns the maximum number of elements this stacks may contain.
// The actual number may be lower because of elided frames. As such, the returned value
// cannot be used to iterate over the frames but may be used to reserve capacity.
func (it *ReverseFrameIterator) LengthUpperBound() int {
return len(it.lines) / 2
}
type Frame struct {
line1 []byte
line2 []byte
}
// UniqueIdentifier can be used as a map key to identify the frame.
func (f *Frame) UniqueIdentifier() []byte {
// line2 contains file path, line number and program-counter offset from the beginning of a function
// e.g. C:/Users/name/scoop/apps/go/current/src/testing/testing.go:1906 +0x63a
return f.line2
}
var createdByPrefix = []byte("created by ")
func (f *Frame) Func() []byte {
if bytes.HasPrefix(f.line1, createdByPrefix) {
// Since go1.21, the line ends with " in goroutine X", saying which goroutine created this one.
// We currently don't have use for that so just remove it.
var line = f.line1[len(createdByPrefix):]
var spaceAt = bytes.IndexByte(line, ' ')
if spaceAt < 0 {
return line
}
return line[:spaceAt]
}
var end = bytes.LastIndexByte(f.line1, '(')
if end >= 0 {
return f.line1[:end]
}
return f.line1
}
func (f *Frame) File() (path []byte, lineNumber int) {
var line = f.line2
if len(line) > 0 && line[0] == '\t' {
line = line[1:]
}
var splitAt = bytes.IndexByte(line, ' ')
if splitAt >= 0 {
line = line[:splitAt]
}
splitAt = bytes.LastIndexByte(line, ':')
if splitAt < 0 {
return line, 0
}
lineNumber, _ = strconv.Atoi(string(line[splitAt+1:]))
return line[:splitAt], lineNumber
}

View file

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

451
vendor/github.com/getsentry/sentry-go/profiler.go generated vendored Normal file
View file

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

View file

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

View file

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

View file

@ -4,7 +4,6 @@ import (
"bytes" "bytes"
"io" "io"
"net/http" "net/http"
"reflect"
"sync" "sync"
"time" "time"
) )
@ -26,13 +25,13 @@ import (
type Scope struct { type Scope struct {
mu sync.RWMutex mu sync.RWMutex
breadcrumbs []*Breadcrumb breadcrumbs []*Breadcrumb
attachments []*Attachment
user User user User
tags map[string]string tags map[string]string
contexts map[string]interface{} contexts map[string]Context
extra map[string]interface{} extra map[string]interface{}
fingerprint []string fingerprint []string
level Level level Level
transaction string
request *http.Request request *http.Request
// requestBody holds a reference to the original request.Body. // requestBody holds a reference to the original request.Body.
requestBody interface { requestBody interface {
@ -50,8 +49,9 @@ type Scope struct {
func NewScope() *Scope { func NewScope() *Scope {
scope := Scope{ scope := Scope{
breadcrumbs: make([]*Breadcrumb, 0), breadcrumbs: make([]*Breadcrumb, 0),
attachments: make([]*Attachment, 0),
tags: make(map[string]string), tags: make(map[string]string),
contexts: make(map[string]interface{}), contexts: make(map[string]Context),
extra: make(map[string]interface{}), extra: make(map[string]interface{}),
fingerprint: make([]string, 0), fingerprint: make([]string, 0),
} }
@ -83,6 +83,22 @@ func (scope *Scope) ClearBreadcrumbs() {
scope.breadcrumbs = []*Breadcrumb{} scope.breadcrumbs = []*Breadcrumb{}
} }
// AddAttachment adds new attachment to the current scope.
func (scope *Scope) AddAttachment(attachment *Attachment) {
scope.mu.Lock()
defer scope.mu.Unlock()
scope.attachments = append(scope.attachments, attachment)
}
// ClearAttachments clears all attachments from the current scope.
func (scope *Scope) ClearAttachments() {
scope.mu.Lock()
defer scope.mu.Unlock()
scope.attachments = []*Attachment{}
}
// SetUser sets the user for the current scope. // SetUser sets the user for the current scope.
func (scope *Scope) SetUser(user User) { func (scope *Scope) SetUser(user User) {
scope.mu.Lock() scope.mu.Lock()
@ -146,7 +162,7 @@ const maxRequestBodyBytes = 10 * 1024
// A limitedBuffer is like a bytes.Buffer, but limited to store at most Capacity // A limitedBuffer is like a bytes.Buffer, but limited to store at most Capacity
// bytes. Any writes past the capacity are silently discarded, similar to // bytes. Any writes past the capacity are silently discarded, similar to
// ioutil.Discard. // io.Discard.
type limitedBuffer struct { type limitedBuffer struct {
Capacity int Capacity int
@ -209,7 +225,7 @@ func (scope *Scope) RemoveTag(key string) {
} }
// SetContext adds a context to the current scope. // SetContext adds a context to the current scope.
func (scope *Scope) SetContext(key string, value interface{}) { func (scope *Scope) SetContext(key string, value Context) {
scope.mu.Lock() scope.mu.Lock()
defer scope.mu.Unlock() defer scope.mu.Unlock()
@ -217,7 +233,7 @@ func (scope *Scope) SetContext(key string, value interface{}) {
} }
// SetContexts assigns multiple contexts to the current scope. // SetContexts assigns multiple contexts to the current scope.
func (scope *Scope) SetContexts(contexts map[string]interface{}) { func (scope *Scope) SetContexts(contexts map[string]Context) {
scope.mu.Lock() scope.mu.Lock()
defer scope.mu.Unlock() defer scope.mu.Unlock()
@ -276,22 +292,6 @@ func (scope *Scope) SetLevel(level Level) {
scope.level = level scope.level = level
} }
// SetTransaction sets the transaction name for the current transaction.
func (scope *Scope) SetTransaction(name string) {
scope.mu.Lock()
defer scope.mu.Unlock()
scope.transaction = name
}
// Transaction returns the transaction name for the current transaction.
func (scope *Scope) Transaction() (name string) {
scope.mu.RLock()
defer scope.mu.RUnlock()
return scope.transaction
}
// Clone returns a copy of the current scope with all data copied over. // Clone returns a copy of the current scope with all data copied over.
func (scope *Scope) Clone() *Scope { func (scope *Scope) Clone() *Scope {
scope.mu.RLock() scope.mu.RLock()
@ -301,11 +301,13 @@ func (scope *Scope) Clone() *Scope {
clone.user = scope.user clone.user = scope.user
clone.breadcrumbs = make([]*Breadcrumb, len(scope.breadcrumbs)) clone.breadcrumbs = make([]*Breadcrumb, len(scope.breadcrumbs))
copy(clone.breadcrumbs, scope.breadcrumbs) copy(clone.breadcrumbs, scope.breadcrumbs)
clone.attachments = make([]*Attachment, len(scope.attachments))
copy(clone.attachments, scope.attachments)
for key, value := range scope.tags { for key, value := range scope.tags {
clone.tags[key] = value clone.tags[key] = value
} }
for key, value := range scope.contexts { for key, value := range scope.contexts {
clone.contexts[key] = value clone.contexts[key] = cloneContext(value)
} }
for key, value := range scope.extra { for key, value := range scope.extra {
clone.extra[key] = value clone.extra[key] = value
@ -313,7 +315,6 @@ func (scope *Scope) Clone() *Scope {
clone.fingerprint = make([]string, len(scope.fingerprint)) clone.fingerprint = make([]string, len(scope.fingerprint))
copy(clone.fingerprint, scope.fingerprint) copy(clone.fingerprint, scope.fingerprint)
clone.level = scope.level clone.level = scope.level
clone.transaction = scope.transaction
clone.request = scope.request clone.request = scope.request
clone.requestBody = scope.requestBody clone.requestBody = scope.requestBody
clone.eventProcessors = scope.eventProcessors clone.eventProcessors = scope.eventProcessors
@ -339,16 +340,16 @@ func (scope *Scope) ApplyToEvent(event *Event, hint *EventHint) *Event {
defer scope.mu.RUnlock() defer scope.mu.RUnlock()
if len(scope.breadcrumbs) > 0 { if len(scope.breadcrumbs) > 0 {
if event.Breadcrumbs == nil {
event.Breadcrumbs = []*Breadcrumb{}
}
event.Breadcrumbs = append(event.Breadcrumbs, scope.breadcrumbs...) event.Breadcrumbs = append(event.Breadcrumbs, scope.breadcrumbs...)
} }
if len(scope.attachments) > 0 {
event.Attachments = append(event.Attachments, scope.attachments...)
}
if len(scope.tags) > 0 { if len(scope.tags) > 0 {
if event.Tags == nil { if event.Tags == nil {
event.Tags = make(map[string]string) event.Tags = make(map[string]string, len(scope.tags))
} }
for key, value := range scope.tags { for key, value := range scope.tags {
@ -358,7 +359,7 @@ func (scope *Scope) ApplyToEvent(event *Event, hint *EventHint) *Event {
if len(scope.contexts) > 0 { if len(scope.contexts) > 0 {
if event.Contexts == nil { if event.Contexts == nil {
event.Contexts = make(map[string]interface{}) event.Contexts = make(map[string]Context)
} }
for key, value := range scope.contexts { for key, value := range scope.contexts {
@ -370,13 +371,17 @@ func (scope *Scope) ApplyToEvent(event *Event, hint *EventHint) *Event {
// to link errors and traces/spans in Sentry. // to link errors and traces/spans in Sentry.
continue continue
} }
event.Contexts[key] = value
// Ensure we are not overwriting event fields
if _, ok := event.Contexts[key]; !ok {
event.Contexts[key] = cloneContext(value)
}
} }
} }
if len(scope.extra) > 0 { if len(scope.extra) > 0 {
if event.Extra == nil { if event.Extra == nil {
event.Extra = make(map[string]interface{}) event.Extra = make(map[string]interface{}, len(scope.extra))
} }
for key, value := range scope.extra { for key, value := range scope.extra {
@ -384,24 +389,18 @@ func (scope *Scope) ApplyToEvent(event *Event, hint *EventHint) *Event {
} }
} }
if (reflect.DeepEqual(event.User, User{})) { if event.User.IsEmpty() {
event.User = scope.user event.User = scope.user
} }
if (event.Fingerprint == nil || len(event.Fingerprint) == 0) && if len(event.Fingerprint) == 0 {
len(scope.fingerprint) > 0 { event.Fingerprint = append(event.Fingerprint, scope.fingerprint...)
event.Fingerprint = make([]string, len(scope.fingerprint))
copy(event.Fingerprint, scope.fingerprint)
} }
if scope.level != "" { if scope.level != "" {
event.Level = scope.level event.Level = scope.level
} }
if scope.transaction != "" {
event.Transaction = scope.transaction
}
if event.Request == nil && scope.request != nil { if event.Request == nil && scope.request != nil {
event.Request = NewRequest(scope.request) event.Request = NewRequest(scope.request)
// NOTE: The SDK does not attempt to send partial request body data. // NOTE: The SDK does not attempt to send partial request body data.
@ -429,3 +428,16 @@ func (scope *Scope) ApplyToEvent(event *Event, hint *EventHint) *Event {
return event return event
} }
// cloneContext returns a new context with keys and values copied from the passed one.
//
// Note: a new Context (map) is returned, but the function does NOT do
// a proper deep copy: if some context values are pointer types (e.g. maps),
// they won't be properly copied.
func cloneContext(c Context) Context {
res := Context{}
for k, v := range c {
res[k] = v
}
return res
}

View file

@ -5,16 +5,13 @@ import (
"time" "time"
) )
// Version is the version of the SDK. // The version of the SDK.
const Version = "0.13.0" const SDKVersion = "0.27.0"
// apiVersion is the minimum version of the Sentry API compatible with the // apiVersion is the minimum version of the Sentry API compatible with the
// sentry-go SDK. // sentry-go SDK.
const apiVersion = "7" const apiVersion = "7"
// userAgent is the User-Agent of outgoing HTTP requests.
const userAgent = "sentry-go/" + Version
// Init initializes the SDK with options. The returned error is non-nil if // Init initializes the SDK with options. The returned error is non-nil if
// options is invalid, for instance if a malformed DSN is provided. // options is invalid, for instance if a malformed DSN is provided.
func Init(options ClientOptions) error { func Init(options ClientOptions) error {
@ -48,6 +45,12 @@ func CaptureException(exception error) *EventID {
return hub.CaptureException(exception) return hub.CaptureException(exception)
} }
// CaptureCheckIn captures a (cron) monitor check-in.
func CaptureCheckIn(checkIn *CheckIn, monitorConfig *MonitorConfig) *EventID {
hub := CurrentHub()
return hub.CaptureCheckIn(checkIn, monitorConfig)
}
// CaptureEvent captures an event on the currently active client if any. // CaptureEvent captures an event on the currently active client if any.
// //
// The event must already be assembled. Typically code would instead use // The event must already be assembled. Typically code would instead use

View file

@ -2,7 +2,7 @@ package sentry
import ( import (
"bytes" "bytes"
"io/ioutil" "os"
"sync" "sync"
) )
@ -24,7 +24,7 @@ func (sr *sourceReader) readContextLines(filename string, line, context int) ([]
lines, ok := sr.cache[filename] lines, ok := sr.cache[filename]
if !ok { if !ok {
data, err := ioutil.ReadFile(filename) data, err := os.ReadFile(filename)
if err != nil { if err != nil {
sr.cache[filename] = nil sr.cache[filename] = nil
return nil, 0 return nil, 0

View file

@ -4,11 +4,6 @@ import (
"sync" "sync"
) )
// maxSpans limits the number of recorded spans per transaction. The limit is
// meant to bound memory usage and prevent too large transaction events that
// would be rejected by Sentry.
const maxSpans = 1000
// A spanRecorder stores a span tree that makes up a transaction. Safe for // A spanRecorder stores a span tree that makes up a transaction. Safe for
// concurrent use. It is okay to add child spans from multiple goroutines. // concurrent use. It is okay to add child spans from multiple goroutines.
type spanRecorder struct { type spanRecorder struct {
@ -20,6 +15,10 @@ type spanRecorder struct {
// record stores a span. The first stored span is assumed to be the root of a // record stores a span. The first stored span is assumed to be the root of a
// span tree. // span tree.
func (r *spanRecorder) record(s *Span) { func (r *spanRecorder) record(s *Span) {
maxSpans := defaultMaxSpans
if client := CurrentHub().Client(); client != nil {
maxSpans = client.options.MaxSpans
}
r.mu.Lock() r.mu.Lock()
defer r.mu.Unlock() defer r.mu.Unlock()
if len(r.spans) >= maxSpans { if len(r.spans) >= maxSpans {

View file

@ -2,7 +2,6 @@ package sentry
import ( import (
"go/build" "go/build"
"path/filepath"
"reflect" "reflect"
"runtime" "runtime"
"strings" "strings"
@ -32,8 +31,8 @@ func NewStacktrace() *Stacktrace {
return nil return nil
} }
frames := extractFrames(pcs[:n]) runtimeFrames := extractFrames(pcs[:n])
frames = filterFrames(frames) frames := createFrames(runtimeFrames)
stacktrace := Stacktrace{ stacktrace := Stacktrace{
Frames: frames, Frames: frames,
@ -62,8 +61,8 @@ func ExtractStacktrace(err error) *Stacktrace {
return nil return nil
} }
frames := extractFrames(pcs) runtimeFrames := extractFrames(pcs)
frames = filterFrames(frames) frames := createFrames(runtimeFrames)
stacktrace := Stacktrace{ stacktrace := Stacktrace{
Frames: frames, Frames: frames,
@ -73,39 +72,38 @@ func ExtractStacktrace(err error) *Stacktrace {
} }
func extractReflectedStacktraceMethod(err error) reflect.Value { func extractReflectedStacktraceMethod(err error) reflect.Value {
var method reflect.Value errValue := reflect.ValueOf(err)
// https://github.com/go-errors/errors
methodStackFrames := errValue.MethodByName("StackFrames")
if methodStackFrames.IsValid() {
return methodStackFrames
}
// https://github.com/pkg/errors
methodStackTrace := errValue.MethodByName("StackTrace")
if methodStackTrace.IsValid() {
return methodStackTrace
}
// https://github.com/pingcap/errors // https://github.com/pingcap/errors
methodGetStackTracer := reflect.ValueOf(err).MethodByName("GetStackTracer") methodGetStackTracer := errValue.MethodByName("GetStackTracer")
// https://github.com/pkg/errors
methodStackTrace := reflect.ValueOf(err).MethodByName("StackTrace")
// https://github.com/go-errors/errors
methodStackFrames := reflect.ValueOf(err).MethodByName("StackFrames")
if methodGetStackTracer.IsValid() { if methodGetStackTracer.IsValid() {
stacktracer := methodGetStackTracer.Call(make([]reflect.Value, 0))[0] stacktracer := methodGetStackTracer.Call(nil)[0]
stacktracerStackTrace := reflect.ValueOf(stacktracer).MethodByName("StackTrace") stacktracerStackTrace := reflect.ValueOf(stacktracer).MethodByName("StackTrace")
if stacktracerStackTrace.IsValid() { if stacktracerStackTrace.IsValid() {
method = stacktracerStackTrace return stacktracerStackTrace
} }
} }
if methodStackTrace.IsValid() { return reflect.Value{}
method = methodStackTrace
}
if methodStackFrames.IsValid() {
method = methodStackFrames
}
return method
} }
func extractPcs(method reflect.Value) []uintptr { func extractPcs(method reflect.Value) []uintptr {
var pcs []uintptr var pcs []uintptr
stacktrace := method.Call(make([]reflect.Value, 0))[0] stacktrace := method.Call(nil)[0]
if stacktrace.Kind() != reflect.Slice { if stacktrace.Kind() != reflect.Slice {
return nil return nil
@ -167,13 +165,7 @@ type Frame struct {
Symbol string `json:"symbol,omitempty"` Symbol string `json:"symbol,omitempty"`
// Module is, despite the name, the Sentry protocol equivalent of a Go // Module is, despite the name, the Sentry protocol equivalent of a Go
// package's import path. // package's import path.
Module string `json:"module,omitempty"` Module string `json:"module,omitempty"`
// Package is not used for Go stack trace frames. In other platforms it
// refers to a container where the Module can be found. For example, a
// Java JAR, a .NET Assembly, or a native dynamic library.
// It exists for completeness, allowing the construction and reporting
// of custom event payloads.
Package string `json:"package,omitempty"`
Filename string `json:"filename,omitempty"` Filename string `json:"filename,omitempty"`
AbsPath string `json:"abs_path,omitempty"` AbsPath string `json:"abs_path,omitempty"`
Lineno int `json:"lineno,omitempty"` Lineno int `json:"lineno,omitempty"`
@ -181,40 +173,24 @@ type Frame struct {
PreContext []string `json:"pre_context,omitempty"` PreContext []string `json:"pre_context,omitempty"`
ContextLine string `json:"context_line,omitempty"` ContextLine string `json:"context_line,omitempty"`
PostContext []string `json:"post_context,omitempty"` PostContext []string `json:"post_context,omitempty"`
InApp bool `json:"in_app,omitempty"` InApp bool `json:"in_app"`
Vars map[string]interface{} `json:"vars,omitempty"` Vars map[string]interface{} `json:"vars,omitempty"`
// Package and the below are not used for Go stack trace frames. In
// other platforms it refers to a container where the Module can be
// found. For example, a Java JAR, a .NET Assembly, or a native
// dynamic library. They exists for completeness, allowing the
// construction and reporting of custom event payloads.
Package string `json:"package,omitempty"`
InstructionAddr string `json:"instruction_addr,omitempty"`
AddrMode string `json:"addr_mode,omitempty"`
SymbolAddr string `json:"symbol_addr,omitempty"`
ImageAddr string `json:"image_addr,omitempty"`
Platform string `json:"platform,omitempty"`
StackStart bool `json:"stack_start,omitempty"`
} }
// NewFrame assembles a stacktrace frame out of runtime.Frame. // NewFrame assembles a stacktrace frame out of runtime.Frame.
func NewFrame(f runtime.Frame) Frame { func NewFrame(f runtime.Frame) Frame {
var abspath, relpath string
// NOTE: f.File paths historically use forward slash as path separator even
// on Windows, though this is not yet documented, see
// https://golang.org/issues/3335. In any case, filepath.IsAbs can work with
// paths with either slash or backslash on Windows.
switch {
case f.File == "":
relpath = unknown
// Leave abspath as the empty string to be omitted when serializing
// event as JSON.
abspath = ""
case filepath.IsAbs(f.File):
abspath = f.File
// TODO: in the general case, it is not trivial to come up with a
// "project relative" path with the data we have in run time.
// We shall not use filepath.Base because it creates ambiguous paths and
// affects the "Suspect Commits" feature.
// For now, leave relpath empty to be omitted when serializing the event
// as JSON. Improve this later.
relpath = ""
default:
// f.File is a relative path. This may happen when the binary is built
// with the -trimpath flag.
relpath = f.File
// Omit abspath when serializing the event as JSON.
abspath = ""
}
function := f.Function function := f.Function
var pkg string var pkg string
@ -222,15 +198,56 @@ func NewFrame(f runtime.Frame) Frame {
pkg, function = splitQualifiedFunctionName(function) pkg, function = splitQualifiedFunctionName(function)
} }
return newFrame(pkg, function, f.File, f.Line)
}
// Like filepath.IsAbs() but doesn't care what platform you run this on.
// I.e. it also recognizies `/path/to/file` when run on Windows.
func isAbsPath(path string) bool {
if len(path) == 0 {
return false
}
// If the volume name starts with a double slash, this is an absolute path.
if len(path) >= 1 && (path[0] == '/' || path[0] == '\\') {
return true
}
// Windows absolute path, see https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats
if len(path) >= 3 && path[1] == ':' && (path[2] == '/' || path[2] == '\\') {
return true
}
return false
}
func newFrame(module string, function string, file string, line int) Frame {
frame := Frame{ frame := Frame{
AbsPath: abspath, Lineno: line,
Filename: relpath, Module: module,
Lineno: f.Line,
Module: pkg,
Function: function, Function: function,
} }
frame.InApp = isInAppFrame(frame) switch {
case len(file) == 0:
frame.Filename = unknown
// Leave abspath as the empty string to be omitted when serializing event as JSON.
case isAbsPath(file):
frame.AbsPath = file
// TODO: in the general case, it is not trivial to come up with a
// "project relative" path with the data we have in run time.
// We shall not use filepath.Base because it creates ambiguous paths and
// affects the "Suspect Commits" feature.
// For now, leave relpath empty to be omitted when serializing the event
// as JSON. Improve this later.
default:
// f.File is a relative path. This may happen when the binary is built
// with the -trimpath flag.
frame.Filename = file
// Omit abspath when serializing the event as JSON.
}
setInAppFrame(&frame)
return frame return frame
} }
@ -240,63 +257,89 @@ func NewFrame(f runtime.Frame) Frame {
// runtime.Frame.Function values. // runtime.Frame.Function values.
func splitQualifiedFunctionName(name string) (pkg string, fun string) { func splitQualifiedFunctionName(name string) (pkg string, fun string) {
pkg = packageName(name) pkg = packageName(name)
fun = strings.TrimPrefix(name, pkg+".") if len(pkg) > 0 {
fun = name[len(pkg)+1:]
}
return return
} }
func extractFrames(pcs []uintptr) []Frame { func extractFrames(pcs []uintptr) []runtime.Frame {
var frames []Frame var frames = make([]runtime.Frame, 0, len(pcs))
callersFrames := runtime.CallersFrames(pcs) callersFrames := runtime.CallersFrames(pcs)
for { for {
callerFrame, more := callersFrames.Next() callerFrame, more := callersFrames.Next()
frames = append([]Frame{ frames = append(frames, callerFrame)
NewFrame(callerFrame),
}, frames...)
if !more { if !more {
break break
} }
} }
// 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]
}
return frames return frames
} }
// filterFrames filters out stack frames that are not meant to be reported to // createFrames creates Frame objects while filtering out frames that are not
// Sentry. Those are frames internal to the SDK or Go. // meant to be reported to Sentry, those are frames internal to the SDK or Go.
func filterFrames(frames []Frame) []Frame { func createFrames(frames []runtime.Frame) []Frame {
if len(frames) == 0 { if len(frames) == 0 {
return nil return nil
} }
filteredFrames := make([]Frame, 0, len(frames)) result := make([]Frame, 0, len(frames))
for _, frame := range frames { for _, frame := range frames {
// Skip Go internal frames. function := frame.Function
if frame.Module == "runtime" || frame.Module == "testing" { var pkg string
continue if function != "" {
pkg, function = splitQualifiedFunctionName(function)
} }
// Skip Sentry internal frames, except for frames in _test packages (for
// testing). if !shouldSkipFrame(pkg) {
if strings.HasPrefix(frame.Module, "github.com/getsentry/sentry-go") && result = append(result, newFrame(pkg, function, frame.File, frame.Line))
!strings.HasSuffix(frame.Module, "_test") {
continue
} }
filteredFrames = append(filteredFrames, frame)
} }
return filteredFrames return result
} }
func isInAppFrame(frame Frame) bool { // TODO ID: why do we want to do this?
if strings.HasPrefix(frame.AbsPath, build.Default.GOROOT) || // I'm not aware of other SDKs skipping all Sentry frames, regardless of their position in the stactrace.
strings.Contains(frame.Module, "vendor") || // For example, in the .NET SDK, only the first frames are skipped until the call to the SDK.
strings.Contains(frame.Module, "third_party") { // As is, this will also hide any intermediate frames in the stack and make debugging issues harder.
return false func shouldSkipFrame(module string) bool {
// Skip Go internal frames.
if module == "runtime" || module == "testing" {
return true
} }
return true // Skip Sentry internal frames, except for frames in _test packages (for testing).
if strings.HasPrefix(module, "github.com/getsentry/sentry-go") &&
!strings.HasSuffix(module, "_test") {
return true
}
return false
}
// On Windows, GOROOT has backslashes, but we want forward slashes.
var goRoot = strings.ReplaceAll(build.Default.GOROOT, "\\", "/")
func setInAppFrame(frame *Frame) {
if strings.HasPrefix(frame.AbsPath, goRoot) ||
strings.Contains(frame.Module, "vendor") ||
strings.Contains(frame.Module, "third_party") {
frame.InApp = false
} else {
frame.InApp = true
}
} }
func callerFunctionName() string { func callerFunctionName() string {
@ -312,9 +355,7 @@ func callerFunctionName() string {
// It replicates https://golang.org/pkg/debug/gosym/#Sym.PackageName, avoiding a // It replicates https://golang.org/pkg/debug/gosym/#Sym.PackageName, avoiding a
// dependency on debug/gosym. // dependency on debug/gosym.
func packageName(name string) string { func packageName(name string) string {
// A prefix of "type." and "go." is a compiler-generated symbol that doesn't belong to any package. if isCompilerGeneratedSymbol(name) {
// See variable reservedimports in cmd/compile/internal/gc/subr.go
if strings.HasPrefix(name, "go.") || strings.HasPrefix(name, "type.") {
return "" return ""
} }

View file

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

View file

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

View file

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

View file

@ -1,171 +1,19 @@
package sentry package sentry
import (
"fmt"
"github.com/getsentry/sentry-go/internal/crypto/randutil"
)
// A TracesSampler makes sampling decisions for spans.
//
// In addition to the sampling context passed to the Sample method,
// implementations may keep and use internal state to make decisions.
//
// Sampling is one of the last steps when starting a new span, such that the
// sampler can inspect most of the state of the span to make a decision.
//
// Implementations must be safe for concurrent use by multiple goroutines.
type TracesSampler interface {
Sample(ctx SamplingContext) Sampled
}
// Implementation note:
//
// TracesSampler.Sample return type is Sampled (instead of bool or float64), so
// that we can compose samplers by letting a sampler return SampledUndefined to
// defer the decision to the next sampler.
//
// For example, a hypothetical InheritFromParentSampler would return
// SampledUndefined if there is no parent span in the SamplingContext, deferring
// the sampling decision to another sampler, like a UniformSampler.
//
// var _ TracesSampler = sentry.TracesSamplers{
// sentry.InheritFromParentSampler,
// sentry.UniformTracesSampler(0.1),
// }
//
// Another example, we can provide a sampler that returns SampledFalse if the
// SamplingContext matches some condition, and SampledUndefined otherwise:
//
// var _ TracesSampler = sentry.TracesSamplers{
// sentry.IgnoreTransaction(regexp.MustCompile(`^\w+ /(favicon.ico|healthz)`),
// sentry.InheritFromParentSampler,
// sentry.UniformTracesSampler(0.1),
// }
//
// If after running all samplers the decision is still undefined, the
// span/transaction is not sampled.
// A SamplingContext is passed to a TracesSampler to determine a sampling // A SamplingContext is passed to a TracesSampler to determine a sampling
// decision. // decision.
//
// TODO(tracing): possibly expand SamplingContext to include custom /
// user-provided data.
type SamplingContext struct { type SamplingContext struct {
Span *Span // The current span, always non-nil. Span *Span // The current span, always non-nil.
Parent *Span // The parent span, may be nil. Parent *Span // The parent span, may be nil.
} }
// TODO(tracing): possibly expand SamplingContext to include custom / // The TracesSample type is an adapter to allow the use of ordinary
// user-provided data.
//
// Unlike in other SDKs, the current http.Request is not part of the
// SamplingContext to avoid bloating it with possibly unnecessary values that
// could confuse people or have negative performance consequences.
//
// For the request to be provided in a SamplingContext, a request pointer would
// most likely need to be stored in the span context and it would open precedent
// for more arbitrary data like fasthttp.Request.
//
// Users wanting to influence the sampling decision based on the request can
// still do so, either by updating the transaction directly on their HTTP
// handler:
//
// func(w http.ResponseWriter, r *http.Request) {
// transaction := sentry.TransactionFromContext(r.Context())
// if r.Header.Get("X-Custom-Sampling") == "yes" {
// transaction.Sampled = sentry.SampledTrue
// } else {
// transaction.Sampled = sentry.SampledFalse
// }
// }
//
// Or by having their own middleware that stores arbitrary data in the request
// context (a pointer to the request itself included):
//
// type myContextKey struct{}
// type myContextData struct {
// request *http.Request
// // ...
// }
//
// func middleware(h http.Handler) http.Handler {
// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// data := &myContextData{
// request: r,
// }
// ctx := context.WithValue(r.Context(), myContextKey{}, data)
// h.ServeHTTP(w, r.WithContext(ctx))
// })
// }
//
// func main() {
// err := sentry.Init(sentry.ClientOptions{
// // A custom TracesSampler can access data from the span's context:
// TracesSampler: sentry.TracesSamplerFunc(func(ctx sentry.SamplingContext) bool {
// data, ok := ctx.Span.Context().Value(myContextKey{}).(*myContextData)
// if !ok {
// return false
// }
// return data.request.URL.Hostname() == "example.com"
// }),
// })
// // ...
// }
//
// Note, however, that for the middleware to be effective, it would have to run
// before sentryhttp's own middleware, meaning the middleware itself is not
// instrumented to send panics to Sentry and it is not part of the timed
// transaction.
//
// If neither of those prove to be sufficient, we can consider including a
// (possibly nil) *http.Request field to SamplingContext. In that case, the SDK
// would need to track the request either in the Scope or the Span.Context.
//
// Alternatively, add a map-like type or simply a generic interface{} similar to
// the CustomSamplingContext type in the Java SDK:
//
// type SamplingContext struct {
// Span *Span // The current span, always non-nil.
// Parent *Span // The parent span, may be nil.
// CustomData interface{}
// }
//
// func CustomSamplingContext(data interface{}) SpanOption {
// return func(s *Span) {
// s.customSamplingContext = data
// }
// }
//
// func main() {
// // ...
// span := sentry.StartSpan(ctx, "op", CustomSamplingContext(data))
// // ...
// }
// The TracesSamplerFunc type is an adapter to allow the use of ordinary
// functions as a TracesSampler. // functions as a TracesSampler.
type TracesSamplerFunc func(ctx SamplingContext) Sampled type TracesSampler func(ctx SamplingContext) float64
var _ TracesSampler = TracesSamplerFunc(nil) func (f TracesSampler) Sample(ctx SamplingContext) float64 {
func (f TracesSamplerFunc) Sample(ctx SamplingContext) Sampled {
return f(ctx) return f(ctx)
} }
// UniformTracesSampler is a TracesSampler that samples root spans randomly at a
// uniform rate.
type UniformTracesSampler float64
var _ TracesSampler = UniformTracesSampler(0)
func (s UniformTracesSampler) Sample(ctx SamplingContext) Sampled {
if s < 0.0 || s > 1.0 {
panic(fmt.Errorf("sampling rate out of range [0.0, 1.0]: %f", s))
}
if randutil.Float64() < float64(s) {
return SampledTrue
}
return SampledFalse
}
// TODO(tracing): implement and export basic TracesSampler implementations:
// parent-based, span ID / trace ID based, etc. It should be possible to compose
// parent-based with other samplers.

View file

@ -9,9 +9,15 @@ import (
"net/http" "net/http"
"regexp" "regexp"
"strings" "strings"
"sync"
"time" "time"
) )
const (
SentryTraceHeader = "sentry-trace"
SentryBaggageHeader = "baggage"
)
// A Span is the building block of a Sentry transaction. Spans build up a tree // A Span is the building block of a Sentry transaction. Spans build up a tree
// structure of timed operations. The span tree makes up a transaction event // structure of timed operations. The span tree makes up a transaction event
// that is sent to Sentry when the root span is finished. // that is sent to Sentry when the root span is finished.
@ -21,6 +27,7 @@ type Span struct { //nolint: maligned // prefer readability over optimal memory
TraceID TraceID `json:"trace_id"` TraceID TraceID `json:"trace_id"`
SpanID SpanID `json:"span_id"` SpanID SpanID `json:"span_id"`
ParentSpanID SpanID `json:"parent_span_id"` ParentSpanID SpanID `json:"parent_span_id"`
Name string `json:"name,omitempty"`
Op string `json:"op,omitempty"` Op string `json:"op,omitempty"`
Description string `json:"description,omitempty"` Description string `json:"description,omitempty"`
Status SpanStatus `json:"status,omitempty"` Status SpanStatus `json:"status,omitempty"`
@ -28,24 +35,43 @@ type Span struct { //nolint: maligned // prefer readability over optimal memory
StartTime time.Time `json:"start_timestamp"` StartTime time.Time `json:"start_timestamp"`
EndTime time.Time `json:"timestamp"` EndTime time.Time `json:"timestamp"`
Data map[string]interface{} `json:"data,omitempty"` Data map[string]interface{} `json:"data,omitempty"`
Sampled Sampled `json:"-"`
Source TransactionSource `json:"-"`
Sampled Sampled `json:"-"` // mu protects concurrent writes to map fields
mu sync.RWMutex
// sample rate the span was sampled with.
sampleRate float64
// ctx is the context where the span was started. Always non-nil. // ctx is the context where the span was started. Always non-nil.
ctx context.Context ctx context.Context
// Dynamic Sampling context
dynamicSamplingContext DynamicSamplingContext
// parent refers to the immediate local parent span. A remote parent span is // parent refers to the immediate local parent span. A remote parent span is
// only referenced by setting ParentSpanID. // only referenced by setting ParentSpanID.
parent *Span parent *Span
// isTransaction is true only for the root span of a local span tree. The
// root span is the first span started in a context. Note that a local root
// span may have a remote parent belonging to the same trace, therefore
// isTransaction depends on ctx and not on parent.
isTransaction bool
// recorder stores all spans in a transaction. Guaranteed to be non-nil. // recorder stores all spans in a transaction. Guaranteed to be non-nil.
recorder *spanRecorder recorder *spanRecorder
// span context, can only be set on transactions
contexts map[string]Context
// collectProfile is a function that collects a profile of the current transaction. May be nil.
collectProfile transactionProfiler
// a Once instance to make sure that Finish() is only called once.
finishOnce sync.Once
}
// TraceParentContext describes the context of a (remote) parent span.
//
// The context is normally extracted from a received "sentry-trace" header and
// used to initialize a new transaction.
//
// Note: the name might be not the best one. It was taken mostly to stay aligned
// with other SDKs, and it alludes to W3C "traceparent" header (https://www.w3.org/TR/trace-context/),
// which serves a similar purpose to "sentry-trace". We should eventually consider
// making this type internal-only and give it a better name.
type TraceParentContext struct {
TraceID TraceID
ParentSpanID SpanID
Sampled Sampled
} }
// (*) Note on maligned: // (*) Note on maligned:
@ -81,14 +107,18 @@ func StartSpan(ctx context.Context, operation string, options ...SpanOption) *Sp
// defaults // defaults
Op: operation, Op: operation,
StartTime: time.Now(), StartTime: time.Now(),
Sampled: SampledUndefined,
ctx: context.WithValue(ctx, spanContextKey{}, &span), ctx: context.WithValue(ctx, spanContextKey{}, &span),
parent: parent, parent: parent,
isTransaction: !hasParent,
} }
if hasParent { if hasParent {
span.TraceID = parent.TraceID span.TraceID = parent.TraceID
} else { } else {
// Only set the Source if this is a transaction
span.Source = SourceCustom
// Implementation note: // Implementation note:
// //
// While math/rand is ~2x faster than crypto/rand (exact // While math/rand is ~2x faster than crypto/rand (exact
@ -146,38 +176,27 @@ func StartSpan(ctx context.Context, operation string, options ...SpanOption) *Sp
} }
span.recorder.record(&span) span.recorder.record(&span)
hub := hubFromContext(ctx)
// Update scope so that all events include a trace context, allowing // Update scope so that all events include a trace context, allowing
// Sentry to correlate errors to transactions/spans. // Sentry to correlate errors to transactions/spans.
hubFromContext(ctx).Scope().SetContext("trace", span.traceContext()) hub.Scope().SetContext("trace", span.traceContext().Map())
// Start profiling only if it's a sampled root transaction.
if span.IsTransaction() && span.Sampled.Bool() {
span.sampleTransactionProfile()
}
return &span return &span
} }
// Finish sets the span's end time, unless already set. If the span is the root // Finish sets the span's end time, unless already set. If the span is the root
// of a span tree, Finish sends the span tree to Sentry as a transaction. // of a span tree, Finish sends the span tree to Sentry as a transaction.
//
// The logic is executed at most once per span, so that (incorrectly) calling it twice
// never double sends to Sentry.
func (s *Span) Finish() { func (s *Span) Finish() {
// TODO(tracing): maybe make Finish run at most once, such that s.finishOnce.Do(s.doFinish)
// (incorrectly) calling it twice never double sends to Sentry.
if s.EndTime.IsZero() {
s.EndTime = monotonicTimeSince(s.StartTime)
}
if !s.Sampled.Bool() {
return
}
event := s.toEvent()
if event == nil {
return
}
// TODO(tracing): add breadcrumbs
// (see https://github.com/getsentry/sentry-python/blob/f6f3525f8812f609/sentry_sdk/tracing.py#L372)
hub := hubFromContext(s.ctx)
if hub.Scope().Transaction() == "" {
Logger.Printf("Missing transaction name for span with op = %q", s.Op)
}
hub.CaptureEvent(event)
} }
// Context returns the context containing the span. // Context returns the context containing the span.
@ -195,12 +214,66 @@ func (s *Span) StartChild(operation string, options ...SpanOption) *Span {
// accessing the tags map directly as SetTag takes care of initializing the map // accessing the tags map directly as SetTag takes care of initializing the map
// when necessary. // when necessary.
func (s *Span) SetTag(name, value string) { func (s *Span) SetTag(name, value string) {
s.mu.Lock()
defer s.mu.Unlock()
if s.Tags == nil { if s.Tags == nil {
s.Tags = make(map[string]string) s.Tags = make(map[string]string)
} }
s.Tags[name] = value s.Tags[name] = value
} }
// SetData sets a data on the span. It is recommended to use SetData instead of
// accessing the data map directly as SetData takes care of initializing the map
// when necessary.
func (s *Span) SetData(name, value string) {
s.mu.Lock()
defer s.mu.Unlock()
if s.Data == nil {
s.Data = make(map[string]interface{})
}
s.Data[name] = value
}
// SetContext sets a context on the span. It is recommended to use SetContext instead of
// accessing the contexts map directly as SetContext takes care of initializing the map
// when necessary.
func (s *Span) SetContext(key string, value Context) {
s.mu.Lock()
defer s.mu.Unlock()
if s.contexts == nil {
s.contexts = make(map[string]Context)
}
s.contexts[key] = value
}
// IsTransaction checks if the given span is a transaction.
func (s *Span) IsTransaction() bool {
return s.parent == nil
}
// GetTransaction returns the transaction that contains this span.
//
// For transaction spans it returns itself. For spans that were created manually
// the method returns "nil".
func (s *Span) GetTransaction() *Span {
spanRecorder := s.spanRecorder()
if spanRecorder == nil {
// This probably means that the Span was created manually (not via
// StartTransaction/StartSpan or StartChild).
// Return "nil" to indicate that it's not a normal situation.
return nil
}
recorderRoot := spanRecorder.root()
if recorderRoot == nil {
// Same as above: manually created Span.
return nil
}
return recorderRoot
}
// TODO(tracing): maybe add shortcuts to get/set transaction name. Right now the // TODO(tracing): maybe add shortcuts to get/set transaction name. Right now the
// transaction name is in the Scope, as it has existed there historically, prior // transaction name is in the Scope, as it has existed there historically, prior
// to tracing. // to tracing.
@ -210,8 +283,9 @@ func (s *Span) SetTag(name, value string) {
// func (s *Span) TransactionName() string // func (s *Span) TransactionName() string
// func (s *Span) SetTransactionName(name string) // func (s *Span) SetTransactionName(name string)
// ToSentryTrace returns the trace propagation value used with the sentry-trace // ToSentryTrace returns the seralized TraceParentContext from a transaction/span.
// HTTP header. // Use this function to propagate the TraceParentContext to a downstream SDK,
// either as the value of the "sentry-trace" HTTP header, or as an html "sentry-trace" meta tag.
func (s *Span) ToSentryTrace() string { func (s *Span) ToSentryTrace() string {
// TODO(tracing): add instrumentation for outgoing HTTP requests using // TODO(tracing): add instrumentation for outgoing HTTP requests using
// ToSentryTrace. // ToSentryTrace.
@ -226,25 +300,76 @@ func (s *Span) ToSentryTrace() string {
return b.String() return b.String()
} }
// ToBaggage returns the serialized DynamicSamplingContext from a transaction.
// Use this function to propagate the DynamicSamplingContext to a downstream SDK,
// either as the value of the "baggage" HTTP header, or as an html "baggage" meta tag.
func (s *Span) ToBaggage() string {
if containingTransaction := s.GetTransaction(); containingTransaction != nil {
// In case there is currently no frozen DynamicSamplingContext attached to the transaction,
// create one from the properties of the transaction.
if !s.dynamicSamplingContext.IsFrozen() {
// This will return a frozen DynamicSamplingContext.
s.dynamicSamplingContext = DynamicSamplingContextFromTransaction(containingTransaction)
}
return containingTransaction.dynamicSamplingContext.String()
}
return ""
}
// SetDynamicSamplingContext sets the given dynamic sampling context on the
// current transaction.
func (s *Span) SetDynamicSamplingContext(dsc DynamicSamplingContext) {
if s.IsTransaction() {
s.dynamicSamplingContext = dsc
}
}
// doFinish runs the actual Span.Finish() logic.
func (s *Span) doFinish() {
if s.EndTime.IsZero() {
s.EndTime = monotonicTimeSince(s.StartTime)
}
if !s.Sampled.Bool() {
return
}
event := s.toEvent()
if event == nil {
return
}
if s.collectProfile != nil {
event.sdkMetaData.transactionProfile = s.collectProfile(s)
}
// TODO(tracing): add breadcrumbs
// (see https://github.com/getsentry/sentry-python/blob/f6f3525f8812f609/sentry_sdk/tracing.py#L372)
hub := hubFromContext(s.ctx)
hub.CaptureEvent(event)
}
// sentryTracePattern matches either // sentryTracePattern matches either
// //
// TRACE_ID - SPAN_ID // TRACE_ID - SPAN_ID
// [[:xdigit:]]{32}-[[:xdigit:]]{16} // [[:xdigit:]]{32}-[[:xdigit:]]{16}
// //
// or // or
// //
// TRACE_ID - SPAN_ID - SAMPLED // TRACE_ID - SPAN_ID - SAMPLED
// [[:xdigit:]]{32}-[[:xdigit:]]{16}-[01] // [[:xdigit:]]{32}-[[:xdigit:]]{16}-[01]
var sentryTracePattern = regexp.MustCompile(`^([[:xdigit:]]{32})-([[:xdigit:]]{16})(?:-([01]))?$`) var sentryTracePattern = regexp.MustCompile(`^([[:xdigit:]]{32})-([[:xdigit:]]{16})(?:-([01]))?$`)
// updateFromSentryTrace parses a sentry-trace HTTP header (as returned by // updateFromSentryTrace parses a sentry-trace HTTP header (as returned by
// ToSentryTrace) and updates fields of the span. If the header cannot be // ToSentryTrace) and updates fields of the span. If the header cannot be
// recognized as valid, the span is left unchanged. // recognized as valid, the span is left unchanged. The returned value indicates
func (s *Span) updateFromSentryTrace(header []byte) { // whether the span was updated.
func (s *Span) updateFromSentryTrace(header []byte) (updated bool) {
m := sentryTracePattern.FindSubmatch(header) m := sentryTracePattern.FindSubmatch(header)
if m == nil { if m == nil {
// no match // no match
return return false
} }
_, _ = hex.Decode(s.TraceID[:], m[1]) _, _ = hex.Decode(s.TraceID[:], m[1])
_, _ = hex.Decode(s.ParentSpanID[:], m[2]) _, _ = hex.Decode(s.ParentSpanID[:], m[2])
@ -256,6 +381,18 @@ func (s *Span) updateFromSentryTrace(header []byte) {
s.Sampled = SampledTrue s.Sampled = SampledTrue
} }
} }
return true
}
func (s *Span) updateFromBaggage(header []byte) {
if s.IsTransaction() {
dsc, err := DynamicSamplingContextFromHeader(header)
if err != nil {
return
}
s.dynamicSamplingContext = dsc
}
} }
func (s *Span) MarshalJSON() ([]byte, error) { func (s *Span) MarshalJSON() ([]byte, error) {
@ -275,46 +412,107 @@ func (s *Span) MarshalJSON() ([]byte, error) {
}) })
} }
func (s *Span) clientOptions() *ClientOptions {
client := hubFromContext(s.ctx).Client()
if client != nil {
return &client.options
}
return &ClientOptions{}
}
func (s *Span) sample() Sampled { func (s *Span) sample() Sampled {
// https://develop.sentry.dev/sdk/unified-api/tracing/#sampling clientOptions := s.clientOptions()
// #1 explicit sampling decision via StartSpan options. // https://develop.sentry.dev/sdk/performance/#sampling
// #1 tracing is not enabled.
if !clientOptions.EnableTracing {
Logger.Printf("Dropping transaction: EnableTracing is set to %t", clientOptions.EnableTracing)
s.sampleRate = 0.0
return SampledFalse
}
// #2 explicit sampling decision via StartSpan/StartTransaction options.
if s.Sampled != SampledUndefined { if s.Sampled != SampledUndefined {
Logger.Printf("Using explicit sampling decision from StartSpan/StartTransaction: %v", s.Sampled)
switch s.Sampled {
case SampledTrue:
s.sampleRate = 1.0
case SampledFalse:
s.sampleRate = 0.0
}
return s.Sampled return s.Sampled
} }
hub := hubFromContext(s.ctx)
var clientOptions ClientOptions
client := hub.Client()
if client != nil {
clientOptions = hub.Client().Options()
}
samplingContext := SamplingContext{Span: s, Parent: s.parent}
// Variant for non-transaction spans: they inherit the parent decision. // Variant for non-transaction spans: they inherit the parent decision.
// TracesSampler only runs for the root span.
// Note: non-transaction should always have a parent, but we check both // Note: non-transaction should always have a parent, but we check both
// conditions anyway -- the first for semantic meaning, the second to // conditions anyway -- the first for semantic meaning, the second to
// avoid a nil pointer dereference. // avoid a nil pointer dereference.
if !s.isTransaction && s.parent != nil { if !s.IsTransaction() && s.parent != nil {
return s.parent.Sampled return s.parent.Sampled
} }
// #2 use TracesSampler from ClientOptions.
// #3 use TracesSampler from ClientOptions.
sampler := clientOptions.TracesSampler sampler := clientOptions.TracesSampler
if sampler != nil { samplingContext := SamplingContext{
return sampler.Sample(samplingContext) Span: s,
Parent: s.parent,
} }
// #3 inherit parent decision.
if sampler != nil {
tracesSamplerSampleRate := sampler.Sample(samplingContext)
s.sampleRate = tracesSamplerSampleRate
if tracesSamplerSampleRate < 0.0 || tracesSamplerSampleRate > 1.0 {
Logger.Printf("Dropping transaction: Returned TracesSampler rate is out of range [0.0, 1.0]: %f", tracesSamplerSampleRate)
return SampledFalse
}
if tracesSamplerSampleRate == 0 {
Logger.Printf("Dropping transaction: Returned TracesSampler rate is: %f", tracesSamplerSampleRate)
return SampledFalse
}
if rng.Float64() < tracesSamplerSampleRate {
return SampledTrue
}
Logger.Printf("Dropping transaction: TracesSampler returned rate: %f", tracesSamplerSampleRate)
return SampledFalse
}
// #4 inherit parent decision.
if s.parent != nil { if s.parent != nil {
Logger.Printf("Using sampling decision from parent: %v", s.parent.Sampled)
switch s.parent.Sampled {
case SampledTrue:
s.sampleRate = 1.0
case SampledFalse:
s.sampleRate = 0.0
}
return s.parent.Sampled return s.parent.Sampled
} }
// #4 uniform sampling using TracesSampleRate.
sampler = UniformTracesSampler(clientOptions.TracesSampleRate) // #5 use TracesSampleRate from ClientOptions.
return sampler.Sample(samplingContext) sampleRate := clientOptions.TracesSampleRate
s.sampleRate = sampleRate
if sampleRate < 0.0 || sampleRate > 1.0 {
Logger.Printf("Dropping transaction: TracesSamplerRate out of range [0.0, 1.0]: %f", sampleRate)
return SampledFalse
}
if sampleRate == 0.0 {
Logger.Printf("Dropping transaction: TracesSampleRate rate is: %f", sampleRate)
return SampledFalse
}
if rng.Float64() < sampleRate {
return SampledTrue
}
return SampledFalse
} }
func (s *Span) toEvent() *Event { func (s *Span) toEvent() *Event {
if !s.isTransaction { s.mu.Lock()
defer s.mu.Unlock()
if !s.IsTransaction() {
return nil // only transactions can be transformed into events return nil // only transactions can be transformed into events
} }
hub := hubFromContext(s.ctx)
children := s.recorder.children() children := s.recorder.children()
finished := make([]*Span, 0, len(children)) finished := make([]*Span, 0, len(children))
@ -326,17 +524,39 @@ func (s *Span) toEvent() *Event {
finished = append(finished, child) finished = append(finished, child)
} }
// Create and attach a DynamicSamplingContext to the transaction.
// If the DynamicSamplingContext is not frozen at this point, we can assume being head of trace.
if !s.dynamicSamplingContext.IsFrozen() {
s.dynamicSamplingContext = DynamicSamplingContextFromTransaction(s)
}
contexts := map[string]Context{}
for k, v := range s.contexts {
contexts[k] = cloneContext(v)
}
contexts["trace"] = s.traceContext().Map()
// Make sure that the transaction source is valid
transactionSource := s.Source
if !transactionSource.isValid() {
transactionSource = SourceCustom
}
return &Event{ return &Event{
Type: transactionType, Type: transactionType,
Transaction: hub.Scope().Transaction(), Transaction: s.Name,
Contexts: map[string]interface{}{ Contexts: contexts,
"trace": s.traceContext(), Tags: s.Tags,
Extra: s.Data,
Timestamp: s.EndTime,
StartTime: s.StartTime,
Spans: finished,
TransactionInfo: &TransactionInfo{
Source: transactionSource,
},
sdkMetaData: SDKMetaData{
dsc: s.dynamicSamplingContext,
}, },
Tags: s.Tags,
Extra: s.Data,
Timestamp: s.EndTime,
StartTime: s.StartTime,
Spans: finished,
} }
} }
@ -354,6 +574,23 @@ func (s *Span) traceContext() *TraceContext {
// spanRecorder stores the span tree. Guaranteed to be non-nil. // spanRecorder stores the span tree. Guaranteed to be non-nil.
func (s *Span) spanRecorder() *spanRecorder { return s.recorder } func (s *Span) spanRecorder() *spanRecorder { return s.recorder }
// ParseTraceParentContext parses a sentry-trace header and builds a TraceParentContext from the
// parsed values. If the header was parsed correctly, the second returned argument
// ("valid") will be set to true, otherwise (e.g., empty or malformed header) it will
// be false.
func ParseTraceParentContext(header []byte) (traceParentContext TraceParentContext, valid bool) {
s := Span{}
updated := s.updateFromSentryTrace(header)
if !updated {
return TraceParentContext{}, false
}
return TraceParentContext{
TraceID: s.TraceID,
ParentSpanID: s.ParentSpanID,
Sampled: s.Sampled,
}, true
}
// TraceID identifies a trace. // TraceID identifies a trace.
type TraceID [16]byte type TraceID [16]byte
@ -394,6 +631,36 @@ var (
zeroSpanID SpanID zeroSpanID SpanID
) )
// Contains information about how the name of the transaction was determined.
type TransactionSource string
const (
SourceCustom TransactionSource = "custom"
SourceURL TransactionSource = "url"
SourceRoute TransactionSource = "route"
SourceView TransactionSource = "view"
SourceComponent TransactionSource = "component"
SourceTask TransactionSource = "task"
)
// A set of all valid transaction sources.
var allTransactionSources = map[TransactionSource]struct{}{
SourceCustom: {},
SourceURL: {},
SourceRoute: {},
SourceView: {},
SourceComponent: {},
SourceTask: {},
}
// isValid returns 'true' if the given transaction source is a valid
// source as recognized by the envelope protocol:
// https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations
func (ts TransactionSource) isValid() bool {
_, found := allTransactionSources[ts]
return found
}
// SpanStatus is the status of a span. // SpanStatus is the status of a span.
type SpanStatus uint8 type SpanStatus uint8
@ -499,15 +766,40 @@ func (tc *TraceContext) MarshalJSON() ([]byte, error) {
}) })
} }
func (tc TraceContext) Map() map[string]interface{} {
m := map[string]interface{}{
"trace_id": tc.TraceID,
"span_id": tc.SpanID,
}
if tc.ParentSpanID != [8]byte{} {
m["parent_span_id"] = tc.ParentSpanID
}
if tc.Op != "" {
m["op"] = tc.Op
}
if tc.Description != "" {
m["description"] = tc.Description
}
if tc.Status > 0 && tc.Status < maxSpanStatus {
m["status"] = tc.Status
}
return m
}
// Sampled signifies a sampling decision. // Sampled signifies a sampling decision.
type Sampled int8 type Sampled int8
// The possible trace sampling decisions are: SampledFalse, SampledUndefined // The possible trace sampling decisions are: SampledFalse, SampledUndefined
// (default) and SampledTrue. // (default) and SampledTrue.
const ( const (
SampledFalse Sampled = -1 + iota SampledFalse Sampled = -1
SampledUndefined SampledUndefined Sampled = 0
SampledTrue SampledTrue Sampled = 1
) )
func (s Sampled) String() string { func (s Sampled) String() string {
@ -531,23 +823,85 @@ func (s Sampled) Bool() bool {
// A SpanOption is a function that can modify the properties of a span. // A SpanOption is a function that can modify the properties of a span.
type SpanOption func(s *Span) type SpanOption func(s *Span)
// The TransactionName option sets the name of the current transaction. // WithTransactionName option sets the name of the current transaction.
// //
// A span tree has a single transaction name, therefore using this option when // A span tree has a single transaction name, therefore using this option when
// starting a span affects the span tree as a whole, potentially overwriting a // starting a span affects the span tree as a whole, potentially overwriting a
// name set previously. // name set previously.
func TransactionName(name string) SpanOption { func WithTransactionName(name string) SpanOption {
return func(s *Span) { return func(s *Span) {
hubFromContext(s.Context()).Scope().SetTransaction(name) s.Name = name
}
}
// WithDescription sets the description of a span.
func WithDescription(description string) SpanOption {
return func(s *Span) {
s.Description = description
}
}
// WithOpName sets the operation name for a given span.
func WithOpName(name string) SpanOption {
return func(s *Span) {
s.Op = name
}
}
// WithTransactionSource sets the source of the transaction name.
//
// Note: if the transaction source is not a valid source (as described
// by the spec https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations),
// it will be corrected to "custom" eventually, before the transaction is sent.
func WithTransactionSource(source TransactionSource) SpanOption {
return func(s *Span) {
s.Source = source
}
}
// WithSpanSampled updates the sampling flag for a given span.
func WithSpanSampled(sampled Sampled) SpanOption {
return func(s *Span) {
s.Sampled = sampled
} }
} }
// ContinueFromRequest returns a span option that updates the span to continue // ContinueFromRequest returns a span option that updates the span to continue
// an existing trace. If it cannot detect an existing trace in the request, the // an existing trace. If it cannot detect an existing trace in the request, the
// span will be left unchanged. // span will be left unchanged.
//
// ContinueFromRequest is an alias for:
//
// ContinueFromHeaders(r.Header.Get(SentryTraceHeader), r.Header.Get(SentryBaggageHeader)).
func ContinueFromRequest(r *http.Request) SpanOption { func ContinueFromRequest(r *http.Request) SpanOption {
return ContinueFromHeaders(r.Header.Get(SentryTraceHeader), r.Header.Get(SentryBaggageHeader))
}
// ContinueFromHeaders returns a span option that updates the span to continue
// an existing TraceID and propagates the Dynamic Sampling context.
func ContinueFromHeaders(trace, baggage string) SpanOption {
return func(s *Span) {
if trace != "" {
s.updateFromSentryTrace([]byte(trace))
}
if baggage != "" {
s.updateFromBaggage([]byte(baggage))
}
// In case a sentry-trace header is present but there are no sentry-related
// values in the baggage, create an empty, frozen DynamicSamplingContext.
if trace != "" && !s.dynamicSamplingContext.HasEntries() {
s.dynamicSamplingContext = DynamicSamplingContext{
Frozen: true,
}
}
}
}
// ContinueFromTrace returns a span option that updates the span to continue
// an existing TraceID.
func ContinueFromTrace(trace string) SpanOption {
return func(s *Span) { return func(s *Span) {
trace := r.Header.Get("sentry-trace")
if trace == "" { if trace == "" {
return return
} }
@ -567,29 +921,65 @@ func TransactionFromContext(ctx context.Context) *Span {
return nil return nil
} }
// spanFromContext returns the last span stored in the context or a dummy // SpanFromContext returns the last span stored in the context, or nil if no span
// non-nil span. // is set on the context.
// func SpanFromContext(ctx context.Context) *Span {
// TODO(tracing): consider exporting this. Without this, users cannot retrieve a
// span from a context since spanContextKey is not exported.
//
// This can be added retroactively, and in the meantime think better whether it
// should return nil (like GetHubFromContext), always non-nil (like
// HubFromContext), or both: two exported functions.
//
// Note the equivalence:
//
// SpanFromContext(ctx).StartChild(...) === StartSpan(ctx, ...)
//
// So we don't aim spanFromContext at creating spans, but mutating existing
// spans that you'd have no access otherwise (because it was created in code you
// do not control, for example SDK auto-instrumentation).
//
// For now we provide TransactionFromContext, which solves the more common case
// of setting tags, etc, on the current transaction.
func spanFromContext(ctx context.Context) *Span {
if span, ok := ctx.Value(spanContextKey{}).(*Span); ok { if span, ok := ctx.Value(spanContextKey{}).(*Span); ok {
return span return span
} }
return nil return nil
} }
// StartTransaction will create a transaction (root span) if there's no existing
// transaction in the context otherwise, it will return the existing transaction.
func StartTransaction(ctx context.Context, name string, options ...SpanOption) *Span {
currentTransaction, exists := ctx.Value(spanContextKey{}).(*Span)
if exists {
return currentTransaction
}
options = append(options, WithTransactionName(name))
return StartSpan(
ctx,
"",
options...,
)
}
// HTTPtoSpanStatus converts an HTTP status code to a SpanStatus.
func HTTPtoSpanStatus(code int) SpanStatus {
if code < http.StatusBadRequest {
return SpanStatusOK
}
if http.StatusBadRequest <= code && code < http.StatusInternalServerError {
switch code {
case http.StatusForbidden:
return SpanStatusPermissionDenied
case http.StatusNotFound:
return SpanStatusNotFound
case http.StatusTooManyRequests:
return SpanStatusResourceExhausted
case http.StatusRequestEntityTooLarge:
return SpanStatusFailedPrecondition
case http.StatusUnauthorized:
return SpanStatusUnauthenticated
case http.StatusConflict:
return SpanStatusAlreadyExists
default:
return SpanStatusInvalidArgument
}
}
if http.StatusInternalServerError <= code && code < 600 {
switch code {
case http.StatusGatewayTimeout:
return SpanStatusDeadlineExceeded
case http.StatusNotImplemented:
return SpanStatusUnimplemented
case http.StatusServiceUnavailable:
return SpanStatusUnavailable
default:
return SpanStatusInternalError
}
}
return SpanStatusUnknown
}

View file

@ -7,7 +7,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"net/http" "net/http"
"net/url" "net/url"
"sync" "sync"
@ -39,11 +38,13 @@ type Transport interface {
func getProxyConfig(options ClientOptions) func(*http.Request) (*url.URL, error) { func getProxyConfig(options ClientOptions) func(*http.Request) (*url.URL, error) {
if options.HTTPSProxy != "" { if options.HTTPSProxy != "" {
return func(_ *http.Request) (*url.URL, error) { return func(*http.Request) (*url.URL, error) {
return url.Parse(options.HTTPSProxy) return url.Parse(options.HTTPSProxy)
} }
} else if options.HTTPProxy != "" { }
return func(_ *http.Request) (*url.URL, error) {
if options.HTTPProxy != "" {
return func(*http.Request) (*url.URL, error) {
return url.Parse(options.HTTPProxy) return url.Parse(options.HTTPProxy)
} }
} }
@ -53,7 +54,7 @@ func getProxyConfig(options ClientOptions) func(*http.Request) (*url.URL, error)
func getTLSConfig(options ClientOptions) *tls.Config { func getTLSConfig(options ClientOptions) *tls.Config {
if options.CaCerts != nil { if options.CaCerts != nil {
//#nosec G402 -- We should be using `MinVersion: tls.VersionTLS12`, // #nosec G402 -- We should be using `MinVersion: tls.VersionTLS12`,
// but we don't want to break peoples code without the major bump. // but we don't want to break peoples code without the major bump.
return &tls.Config{ return &tls.Config{
RootCAs: options.CaCerts, RootCAs: options.CaCerts,
@ -93,64 +94,149 @@ func getRequestBodyFromEvent(event *Event) []byte {
return nil return nil
} }
func transactionEnvelopeFromBody(eventID EventID, sentAt time.Time, body json.RawMessage) (*bytes.Buffer, error) { func encodeAttachment(enc *json.Encoder, b io.Writer, attachment *Attachment) error {
var b bytes.Buffer // Attachment header
enc := json.NewEncoder(&b)
// envelope header
err := enc.Encode(struct { err := enc.Encode(struct {
EventID EventID `json:"event_id"` Type string `json:"type"`
SentAt time.Time `json:"sent_at"` Length int `json:"length"`
Filename string `json:"filename"`
ContentType string `json:"content_type,omitempty"`
}{ }{
EventID: eventID, Type: "attachment",
SentAt: sentAt, Length: len(attachment.Payload),
Filename: attachment.Filename,
ContentType: attachment.ContentType,
}) })
if err != nil { if err != nil {
return nil, err return err
} }
// item header
err = enc.Encode(struct { // Attachment payload
if _, err = b.Write(attachment.Payload); err != nil {
return err
}
// "Envelopes should be terminated with a trailing newline."
//
// [1]: https://develop.sentry.dev/sdk/envelopes/#envelopes
if _, err := b.Write([]byte("\n")); err != nil {
return err
}
return nil
}
func encodeEnvelopeItem(enc *json.Encoder, itemType string, body json.RawMessage) error {
// Item header
err := enc.Encode(struct {
Type string `json:"type"` Type string `json:"type"`
Length int `json:"length"` Length int `json:"length"`
}{ }{
Type: transactionType, Type: itemType,
Length: len(body), Length: len(body),
}) })
if err == nil {
// payload
err = enc.Encode(body)
}
return err
}
func envelopeFromBody(event *Event, dsn *Dsn, sentAt time.Time, body json.RawMessage) (*bytes.Buffer, error) {
var b bytes.Buffer
enc := json.NewEncoder(&b)
// Construct the trace envelope header
var trace = map[string]string{}
if dsc := event.sdkMetaData.dsc; dsc.HasEntries() {
for k, v := range dsc.Entries {
trace[k] = v
}
}
// Envelope header
err := enc.Encode(struct {
EventID EventID `json:"event_id"`
SentAt time.Time `json:"sent_at"`
Dsn string `json:"dsn"`
Sdk map[string]string `json:"sdk"`
Trace map[string]string `json:"trace,omitempty"`
}{
EventID: event.EventID,
SentAt: sentAt,
Trace: trace,
Dsn: dsn.String(),
Sdk: map[string]string{
"name": event.Sdk.Name,
"version": event.Sdk.Version,
},
})
if err != nil { if err != nil {
return nil, err return nil, err
} }
// payload
err = enc.Encode(body) if event.Type == transactionType || event.Type == checkInType {
err = encodeEnvelopeItem(enc, event.Type, body)
} else {
err = encodeEnvelopeItem(enc, eventType, body)
}
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Attachments
for _, attachment := range event.Attachments {
if err := encodeAttachment(enc, &b, attachment); err != nil {
return nil, err
}
}
// Profile data
if event.sdkMetaData.transactionProfile != nil {
body, err = json.Marshal(event.sdkMetaData.transactionProfile)
if err != nil {
return nil, err
}
err = encodeEnvelopeItem(enc, profileType, body)
if err != nil {
return nil, err
}
}
return &b, nil return &b, nil
} }
func getRequestFromEvent(event *Event, dsn *Dsn) (r *http.Request, err error) { func getRequestFromEvent(event *Event, dsn *Dsn) (r *http.Request, err error) {
defer func() { defer func() {
if r != nil { if r != nil {
r.Header.Set("User-Agent", userAgent) r.Header.Set("User-Agent", fmt.Sprintf("%s/%s", event.Sdk.Name, event.Sdk.Version))
r.Header.Set("Content-Type", "application/x-sentry-envelope")
auth := fmt.Sprintf("Sentry sentry_version=%s, "+
"sentry_client=%s/%s, sentry_key=%s", apiVersion, event.Sdk.Name, event.Sdk.Version, dsn.publicKey)
// The key sentry_secret is effectively deprecated and no longer needs to be set.
// However, since it was required in older self-hosted versions,
// it should still passed through to Sentry if set.
if dsn.secretKey != "" {
auth = fmt.Sprintf("%s, sentry_secret=%s", auth, dsn.secretKey)
}
r.Header.Set("X-Sentry-Auth", auth)
} }
}() }()
body := getRequestBodyFromEvent(event) body := getRequestBodyFromEvent(event)
if body == nil { if body == nil {
return nil, errors.New("event could not be marshaled") return nil, errors.New("event could not be marshaled")
} }
if event.Type == transactionType { envelope, err := envelopeFromBody(event, dsn, time.Now(), body)
b, err := transactionEnvelopeFromBody(event.EventID, time.Now(), body) if err != nil {
if err != nil { return nil, err
return nil, err
}
return http.NewRequest(
http.MethodPost,
dsn.EnvelopeAPIURL().String(),
b,
)
} }
return http.NewRequest( return http.NewRequest(
http.MethodPost, http.MethodPost,
dsn.StoreAPIURL().String(), dsn.GetAPIURL().String(),
bytes.NewReader(body), envelope,
) )
} }
@ -275,10 +361,6 @@ func (t *HTTPTransport) SendEvent(event *Event) {
return return
} }
for headerKey, headerValue := range t.dsn.RequestHeaders() {
request.Header.Set(headerKey, headerValue)
}
// <-t.buffer is equivalent to acquiring a lock to access the current batch. // <-t.buffer is equivalent to acquiring a lock to access the current batch.
// A few lines below, t.buffer <- b releases the lock. // A few lines below, t.buffer <- b releases the lock.
// //
@ -401,7 +483,7 @@ func (t *HTTPTransport) worker() {
t.mu.Unlock() t.mu.Unlock()
// Drain body up to a limit and close it, allowing the // Drain body up to a limit and close it, allowing the
// transport to reuse TCP connections. // transport to reuse TCP connections.
_, _ = io.CopyN(ioutil.Discard, response.Body, maxDrainResponseBytes) _, _ = io.CopyN(io.Discard, response.Body, maxDrainResponseBytes)
response.Body.Close() response.Body.Close()
} }
@ -500,10 +582,6 @@ func (t *HTTPSyncTransport) SendEvent(event *Event) {
return return
} }
for headerKey, headerValue := range t.dsn.RequestHeaders() {
request.Header.Set(headerKey, headerValue)
}
var eventType string var eventType string
if event.Type == transactionType { if event.Type == transactionType {
eventType = "transaction" eventType = "transaction"
@ -529,7 +607,7 @@ func (t *HTTPSyncTransport) SendEvent(event *Event) {
// Drain body up to a limit and close it, allowing the // Drain body up to a limit and close it, allowing the
// transport to reuse TCP connections. // transport to reuse TCP connections.
_, _ = io.CopyN(ioutil.Discard, response.Body, maxDrainResponseBytes) _, _ = io.CopyN(io.Discard, response.Body, maxDrainResponseBytes)
response.Body.Close() response.Body.Close()
} }
@ -556,14 +634,16 @@ func (t *HTTPSyncTransport) disabled(c ratelimit.Category) bool {
// Only used internally when an empty DSN is provided, which effectively disables the SDK. // Only used internally when an empty DSN is provided, which effectively disables the SDK.
type noopTransport struct{} type noopTransport struct{}
func (t *noopTransport) Configure(options ClientOptions) { var _ Transport = noopTransport{}
func (noopTransport) Configure(ClientOptions) {
Logger.Println("Sentry client initialized with an empty DSN. Using noopTransport. No events will be delivered.") Logger.Println("Sentry client initialized with an empty DSN. Using noopTransport. No events will be delivered.")
} }
func (t *noopTransport) SendEvent(event *Event) { func (noopTransport) SendEvent(*Event) {
Logger.Println("Event dropped due to noopTransport usage.") Logger.Println("Event dropped due to noopTransport usage.")
} }
func (t *noopTransport) Flush(_ time.Duration) bool { func (noopTransport) Flush(time.Duration) bool {
return true return true
} }

View file

@ -6,6 +6,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
"runtime/debug"
"strings" "strings"
"time" "time"
@ -35,7 +36,7 @@ func monotonicTimeSince(start time.Time) (end time.Time) {
return start.Add(time.Since(start)) return start.Add(time.Since(start))
} }
//nolint: deadcode, unused // nolint: deadcode, unused
func prettyPrint(data interface{}) { func prettyPrint(data interface{}) {
dbg, _ := json.MarshalIndent(data, "", " ") dbg, _ := json.MarshalIndent(data, "", " ")
fmt.Println(string(dbg)) fmt.Println(string(dbg))
@ -66,26 +67,48 @@ func defaultRelease() (release string) {
} }
} }
if info, ok := debug.ReadBuildInfo(); ok {
buildInfoVcsRevision := revisionFromBuildInfo(info)
if len(buildInfoVcsRevision) > 0 {
return buildInfoVcsRevision
}
}
// Derive a version string from Git. Example outputs: // Derive a version string from Git. Example outputs:
// v1.0.1-0-g9de4 // v1.0.1-0-g9de4
// v2.0-8-g77df-dirty // v2.0-8-g77df-dirty
// 4f72d7 // 4f72d7
cmd := exec.Command("git", "describe", "--long", "--always", "--dirty") if _, err := exec.LookPath("git"); err == nil {
b, err := cmd.Output() cmd := exec.Command("git", "describe", "--long", "--always", "--dirty")
if err != nil { b, err := cmd.Output()
// Either Git is not available or the current directory is not a if err != nil {
// Git repository. // Either Git is not available or the current directory is not a
var s strings.Builder // Git repository.
fmt.Fprintf(&s, "Release detection failed: %v", err) var s strings.Builder
if err, ok := err.(*exec.ExitError); ok && len(err.Stderr) > 0 { fmt.Fprintf(&s, "Release detection failed: %v", err)
fmt.Fprintf(&s, ": %s", err.Stderr) if err, ok := err.(*exec.ExitError); ok && len(err.Stderr) > 0 {
fmt.Fprintf(&s, ": %s", err.Stderr)
}
Logger.Print(s.String())
} else {
release = strings.TrimSpace(string(b))
Logger.Printf("Using release from Git: %s", release)
return release
} }
Logger.Print(s.String())
Logger.Print("Some Sentry features will not be available. See https://docs.sentry.io/product/releases/.")
Logger.Print("To stop seeing this message, pass a Release to sentry.Init or set the SENTRY_RELEASE environment variable.")
return ""
} }
release = strings.TrimSpace(string(b))
Logger.Printf("Using release from Git: %s", release) Logger.Print("Some Sentry features will not be available. See https://docs.sentry.io/product/releases/.")
return release Logger.Print("To stop seeing this message, pass a Release to sentry.Init or set the SENTRY_RELEASE environment variable.")
return ""
}
func revisionFromBuildInfo(info *debug.BuildInfo) string {
for _, setting := range info.Settings {
if setting.Key == "vcs.revision" && setting.Value != "" {
Logger.Printf("Using release from debug info: %s", setting.Value)
return setting.Value
}
}
return ""
} }

Some files were not shown because too many files have changed in this diff Show more