server: report HTTP handler panics to Sentry (closes #107) #184
@@ -359,6 +359,13 @@ either is set to anything else, including a bare number or a zero or negative
|
|||||||
duration, dnswatcher refuses to start with an error naming the variable and
|
duration, dnswatcher refuses to start with an error naming the variable and
|
||||||
the value.
|
the value.
|
||||||
|
|
||||||
|
**`DNSWATCHER_SENTRY_DSN` reports crashes in HTTP requests to Sentry.** When it
|
||||||
|
is set, a panic in an HTTP request handler is sent to Sentry, and the request
|
||||||
|
still gets a `500 Internal Server Error` answer. Nothing else is sent to Sentry:
|
||||||
|
DNS, port and TLS problems are reported as notifications. A value Sentry cannot
|
||||||
|
parse stops dnswatcher at startup. At shutdown, reports not yet sent are sent,
|
||||||
|
waiting at most 2 seconds.
|
||||||
|
|
||||||
### Example `.env`
|
### Example `.env`
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ nameserver IP address changes: https://git.eeqj.de/sneak/dnswatcher/issues/105
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
|
- 2026-10-01: `DNSWATCHER_SENTRY_DSN` reports panics in HTTP handlers to Sentry,
|
||||||
|
and a DSN Sentry cannot parse stops startup (closes #107).
|
||||||
- 2026-10-01: a port or TLS check that shutdown cuts short saves nothing and
|
- 2026-10-01: a port or TLS check that shutdown cuts short saves nothing and
|
||||||
sends no notification, as a cut-short DNS lookup already did (closes #185).
|
sends no notification, as a cut-short DNS lookup already did (closes #185).
|
||||||
- 2026-10-01: the client address from `X-Forwarded-For` is the last entry that
|
- 2026-10-01: the client address from `X-Forwarded-For` is the last entry that
|
||||||
@@ -101,8 +103,6 @@ nameserver IP address changes: https://git.eeqj.de/sneak/dnswatcher/issues/105
|
|||||||
|
|
||||||
# Future Steps
|
# Future Steps
|
||||||
|
|
||||||
- `DNSWATCHER_SENTRY_DSN` does nothing:
|
|
||||||
https://git.eeqj.de/sneak/dnswatcher/issues/107
|
|
||||||
- trial run of the finished image:
|
- trial run of the finished image:
|
||||||
https://git.eeqj.de/sneak/dnswatcher/issues/149
|
https://git.eeqj.de/sneak/dnswatcher/issues/149
|
||||||
- 1.0 readiness: run it with a real config and read the logs:
|
- 1.0 readiness: run it with a real config and read the logs:
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ go 1.25.5
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/99designs/basicauth-go v0.0.0-20230316000542-bf6f9cbbf0f8
|
github.com/99designs/basicauth-go v0.0.0-20230316000542-bf6f9cbbf0f8
|
||||||
|
github.com/getsentry/sentry-go v0.49.0
|
||||||
github.com/go-chi/chi/v5 v5.2.5
|
github.com/go-chi/chi/v5 v5.2.5
|
||||||
github.com/go-chi/cors v1.2.2
|
github.com/go-chi/cors v1.2.2
|
||||||
github.com/go-chi/httprate v0.16.0
|
github.com/go-chi/httprate v0.16.0
|
||||||
@@ -13,20 +14,20 @@ require (
|
|||||||
github.com/spf13/viper v1.21.0
|
github.com/spf13/viper v1.21.0
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
go.uber.org/fx v1.24.0
|
go.uber.org/fx v1.24.0
|
||||||
golang.org/x/net v0.50.0
|
golang.org/x/net v0.56.0
|
||||||
golang.org/x/sync v0.19.0
|
golang.org/x/sync v0.21.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
github.com/prometheus/client_model v0.6.2 // indirect
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
github.com/prometheus/common v0.66.1 // indirect
|
github.com/prometheus/common v0.66.1 // indirect
|
||||||
github.com/prometheus/procfs v0.16.1 // indirect
|
github.com/prometheus/procfs v0.16.1 // indirect
|
||||||
@@ -42,10 +43,10 @@ require (
|
|||||||
go.uber.org/zap v1.26.0 // indirect
|
go.uber.org/zap v1.26.0 // indirect
|
||||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
golang.org/x/mod v0.32.0 // indirect
|
golang.org/x/mod v0.37.0 // indirect
|
||||||
golang.org/x/sys v0.41.0 // indirect
|
golang.org/x/sys v0.46.0 // indirect
|
||||||
golang.org/x/text v0.34.0 // indirect
|
golang.org/x/text v0.39.0 // indirect
|
||||||
golang.org/x/tools v0.41.0 // indirect
|
golang.org/x/tools v0.47.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.8 // indirect
|
google.golang.org/protobuf v1.36.8 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,18 +4,22 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
|||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
|
github.com/getsentry/sentry-go v0.49.0 h1:Ehejknu1l023Ub7QoRBVLAI7g3Jnhqku4oWx4B4Sh5s=
|
||||||
|
github.com/getsentry/sentry-go v0.49.0/go.mod h1:nuMJAoCfe1u0Bts2ocyNI+TW8HT84vRMqwA5Qq/SKUI=
|
||||||
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
|
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
|
||||||
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
||||||
github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
|
github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
|
||||||
github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
||||||
github.com/go-chi/httprate v0.16.0 h1:8V5DH9j6pSK6UQoBsTpvMyFxycqaKEIToyPKzHJjUa8=
|
github.com/go-chi/httprate v0.16.0 h1:8V5DH9j6pSK6UQoBsTpvMyFxycqaKEIToyPKzHJjUa8=
|
||||||
github.com/go-chi/httprate v0.16.0/go.mod h1:A8lo+qRhk+s9LiuP5saS7XCGDXRXMcrueq0NfIuCa/I=
|
github.com/go-chi/httprate v0.16.0/go.mod h1:A8lo+qRhk+s9LiuP5saS7XCGDXRXMcrueq0NfIuCa/I=
|
||||||
|
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
||||||
|
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
@@ -38,8 +42,12 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
|
|||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||||
@@ -48,8 +56,8 @@ github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9Z
|
|||||||
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
|
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
|
||||||
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
||||||
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
||||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
|
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
|
||||||
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
|
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
|
||||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
||||||
@@ -84,18 +92,18 @@ go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
|||||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
|
||||||
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
|
||||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
||||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
||||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus=
|
||||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM=
|
||||||
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
|
||||||
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
|
||||||
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||||
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||||
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=
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package server
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RequestTimeout exports the handler execution budget applied by
|
// RequestTimeout exports the handler execution budget applied by
|
||||||
@@ -22,3 +24,15 @@ func SetListenPort(s *Server, port int) {
|
|||||||
func HTTPServerOf(s *Server) *http.Server {
|
func HTTPServerOf(s *Server) *http.Server {
|
||||||
return s.httpServer
|
return s.httpServer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EnableSentry runs the Sentry setup that the start hook runs, without
|
||||||
|
// starting the HTTP server.
|
||||||
|
func EnableSentry(s *Server) error {
|
||||||
|
return s.enableSentry()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RouterOf returns the router SetupRoutes built, so a test can add a
|
||||||
|
// route that panics.
|
||||||
|
func RouterOf(s *Server) *chi.Mux {
|
||||||
|
return s.router
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
sentryhttp "github.com/getsentry/sentry-go/http"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
chimw "github.com/go-chi/chi/v5/middleware"
|
chimw "github.com/go-chi/chi/v5/middleware"
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
@@ -25,6 +26,16 @@ func (s *Server) SetupRoutes() {
|
|||||||
s.router.Use(s.mw.Logging())
|
s.router.Use(s.mw.Logging())
|
||||||
s.router.Use(chimw.Timeout(requestTimeout))
|
s.router.Use(chimw.Timeout(requestTimeout))
|
||||||
|
|
||||||
|
// Report panics in handlers to Sentry when DNSWATCHER_SENTRY_DSN is
|
||||||
|
// set. Repanic passes each panic on to chimw.Recoverer above, which
|
||||||
|
// still answers the request.
|
||||||
|
if s.sentryEnabled {
|
||||||
|
sentryHandler := sentryhttp.New(sentryhttp.Options{
|
||||||
|
Repanic: true,
|
||||||
|
})
|
||||||
|
s.router.Use(sentryHandler.Handle)
|
||||||
|
}
|
||||||
|
|
||||||
// Public, unauthenticated, read-only routes, the only ones
|
// Public, unauthenticated, read-only routes, the only ones
|
||||||
// REPO_POLICIES.md allows wildcard CORS on. CORS is middleware of
|
// REPO_POLICIES.md allows wildcard CORS on. CORS is middleware of
|
||||||
// this whole router, not of a Group, so that it also answers
|
// this whole router, not of a Group, so that it also answers
|
||||||
|
|||||||
@@ -0,0 +1,190 @@
|
|||||||
|
package server_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/getsentry/sentry-go"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"go.uber.org/fx"
|
||||||
|
|
||||||
|
"sneak.berlin/go/dnswatcher/internal/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The tests below set env vars and touch the global state of viper and
|
||||||
|
// of Sentry, so they cannot use t.Parallel.
|
||||||
|
|
||||||
|
// standInDelay is how long the Sentry stand-in takes to answer. It
|
||||||
|
// records a report only then, so a report that Shutdown did not wait
|
||||||
|
// for has not been recorded yet when Shutdown returns.
|
||||||
|
const standInDelay = 100 * time.Millisecond
|
||||||
|
|
||||||
|
// sentryStandIn is a local HTTP server in place of Sentry's, so that
|
||||||
|
// nothing a test reports leaves the host. It keeps the body of every
|
||||||
|
// request it receives.
|
||||||
|
type sentryStandIn struct {
|
||||||
|
server *httptest.Server
|
||||||
|
mu sync.Mutex
|
||||||
|
bodies []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSentryStandIn(t *testing.T) *sentryStandIn {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
standIn := &sentryStandIn{}
|
||||||
|
standIn.server = httptest.NewServer(http.HandlerFunc(
|
||||||
|
func(_ http.ResponseWriter, r *http.Request) {
|
||||||
|
body, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("reading request to the Sentry stand-in: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(standInDelay)
|
||||||
|
|
||||||
|
standIn.mu.Lock()
|
||||||
|
defer standIn.mu.Unlock()
|
||||||
|
|
||||||
|
standIn.bodies = append(standIn.bodies, string(body))
|
||||||
|
},
|
||||||
|
))
|
||||||
|
t.Cleanup(standIn.server.Close)
|
||||||
|
|
||||||
|
return standIn
|
||||||
|
}
|
||||||
|
|
||||||
|
// dsn returns a DSN that points Sentry at the stand-in.
|
||||||
|
func (s *sentryStandIn) dsn(t *testing.T) string {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
dsn, err := url.Parse(s.server.URL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parsing the stand-in URL: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dsn.User = url.User("public-key")
|
||||||
|
dsn.Path = "/1"
|
||||||
|
|
||||||
|
return dsn.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// received reports whether a request to the stand-in contained text.
|
||||||
|
func (s *sentryStandIn) received(text string) bool {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
for _, body := range s.bodies {
|
||||||
|
if strings.Contains(body, text) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSentryUnsetDoesNothing(t *testing.T) {
|
||||||
|
viper.Reset()
|
||||||
|
t.Setenv("DNSWATCHER_TARGETS", "example.com")
|
||||||
|
t.Setenv("DNSWATCHER_SENTRY_DSN", "")
|
||||||
|
|
||||||
|
srv := buildServer(t)
|
||||||
|
|
||||||
|
err := server.EnableSentry(srv)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Sentry setup with no DSN: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if sentry.CurrentHub().Client() != nil {
|
||||||
|
t.Error("Sentry was set up with no DSN configured")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSentryReportsHandlerPanic checks that with a valid DSN a panic in
|
||||||
|
// a handler is reported to Sentry, still reaches chimw.Recoverer, and
|
||||||
|
// has been sent by the time Shutdown returns, and that nothing else is
|
||||||
|
// sent to Sentry.
|
||||||
|
func TestSentryReportsHandlerPanic(t *testing.T) {
|
||||||
|
standIn := newSentryStandIn(t)
|
||||||
|
|
||||||
|
viper.Reset()
|
||||||
|
t.Setenv("DNSWATCHER_TARGETS", "example.com")
|
||||||
|
t.Setenv("DNSWATCHER_SENTRY_DSN", standIn.dsn(t))
|
||||||
|
|
||||||
|
srv := buildServer(t)
|
||||||
|
|
||||||
|
err := server.EnableSentry(srv)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Sentry setup with a valid DSN: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sentry's client is global: close it so later tests find none.
|
||||||
|
t.Cleanup(func() {
|
||||||
|
sentry.CurrentHub().Client().Close()
|
||||||
|
sentry.CurrentHub().BindClient(nil)
|
||||||
|
})
|
||||||
|
|
||||||
|
const panicMessage = "handler panic in the Sentry test"
|
||||||
|
|
||||||
|
srv.SetupRoutes()
|
||||||
|
server.RouterOf(srv).Get(
|
||||||
|
"/panic",
|
||||||
|
func(http.ResponseWriter, *http.Request) {
|
||||||
|
panic(panicMessage)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// An ordinary request first: with client reports on, Sentry would
|
||||||
|
// add a count of its dropped transaction to the panic report.
|
||||||
|
serve(srv, httptest.NewRequestWithContext(
|
||||||
|
t.Context(), http.MethodGet, "/.well-known/healthcheck", nil,
|
||||||
|
))
|
||||||
|
|
||||||
|
rec := serve(srv, httptest.NewRequestWithContext(
|
||||||
|
t.Context(), http.MethodGet, "/panic", nil,
|
||||||
|
))
|
||||||
|
if rec.Code != http.StatusInternalServerError {
|
||||||
|
t.Errorf(
|
||||||
|
"status %d, want %d from the recoverer",
|
||||||
|
rec.Code, http.StatusInternalServerError,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = srv.Shutdown(t.Context())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Shutdown: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !standIn.received(panicMessage) {
|
||||||
|
t.Error("the panic had not been sent to Sentry when Shutdown returned")
|
||||||
|
}
|
||||||
|
|
||||||
|
if standIn.received("client_report") {
|
||||||
|
t.Error("Sentry was sent a client report, not only the panic")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSentryInvalidDSNStopsStartup(t *testing.T) {
|
||||||
|
viper.Reset()
|
||||||
|
t.Setenv("DNSWATCHER_TARGETS", "example.com")
|
||||||
|
t.Setenv("DNSWATCHER_DATA_DIR", t.TempDir())
|
||||||
|
// Sentry cannot parse this: it has no public key before the host.
|
||||||
|
t.Setenv("DNSWATCHER_SENTRY_DSN", "https://sentry.test/1")
|
||||||
|
|
||||||
|
app := newServerApp(fx.Invoke(func(*server.Server) {}))
|
||||||
|
|
||||||
|
err := app.Start(t.Context())
|
||||||
|
if err == nil {
|
||||||
|
_ = app.Stop(t.Context())
|
||||||
|
|
||||||
|
t.Fatal("startup succeeded with an invalid DSN")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(err.Error(), "invalid DNSWATCHER_SENTRY_DSN") {
|
||||||
|
t.Errorf("startup error does not name the setting: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/getsentry/sentry-go"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"go.uber.org/fx"
|
"go.uber.org/fx"
|
||||||
|
|
||||||
@@ -33,6 +34,10 @@ type Params struct {
|
|||||||
// shutdownTimeout is how long to wait for graceful shutdown.
|
// shutdownTimeout is how long to wait for graceful shutdown.
|
||||||
const shutdownTimeout = 30 * time.Second
|
const shutdownTimeout = 30 * time.Second
|
||||||
|
|
||||||
|
// sentryFlushTimeout is how long shutdown waits for Sentry to send the
|
||||||
|
// error reports it still holds.
|
||||||
|
const sentryFlushTimeout = 2 * time.Second
|
||||||
|
|
||||||
// Socket-level timeouts for the HTTP server.
|
// Socket-level timeouts for the HTTP server.
|
||||||
//
|
//
|
||||||
// These bound time spent on the connection itself and are a distinct
|
// These bound time spent on the connection itself and are a distinct
|
||||||
@@ -82,14 +87,15 @@ const (
|
|||||||
|
|
||||||
// Server is the HTTP server.
|
// Server is the HTTP server.
|
||||||
type Server struct {
|
type Server struct {
|
||||||
startupTime time.Time
|
startupTime time.Time
|
||||||
port int
|
port int
|
||||||
log *slog.Logger
|
sentryEnabled bool
|
||||||
router *chi.Mux
|
log *slog.Logger
|
||||||
httpServer *http.Server
|
router *chi.Mux
|
||||||
params Params
|
httpServer *http.Server
|
||||||
mw *middleware.Middleware
|
params Params
|
||||||
handlers *handlers.Handlers
|
mw *middleware.Middleware
|
||||||
|
handlers *handlers.Handlers
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new Server instance.
|
// New creates a new Server instance.
|
||||||
@@ -108,6 +114,12 @@ func New(
|
|||||||
lifecycle.Append(fx.Hook{
|
lifecycle.Append(fx.Hook{
|
||||||
OnStart: func(_ context.Context) error {
|
OnStart: func(_ context.Context) error {
|
||||||
srv.startupTime = time.Now()
|
srv.startupTime = time.Now()
|
||||||
|
|
||||||
|
err := srv.enableSentry()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
go srv.Run()
|
go srv.Run()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -152,8 +164,11 @@ func (s *Server) Run() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shutdown gracefully shuts down the server.
|
// Shutdown gracefully shuts down the server, then sends the error
|
||||||
|
// reports Sentry still holds.
|
||||||
func (s *Server) Shutdown(ctx context.Context) error {
|
func (s *Server) Shutdown(ctx context.Context) error {
|
||||||
|
defer s.flushSentry()
|
||||||
|
|
||||||
if s.httpServer == nil {
|
if s.httpServer == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -184,3 +199,45 @@ func (s *Server) ServeHTTP(
|
|||||||
) {
|
) {
|
||||||
s.router.ServeHTTP(writer, request)
|
s.router.ServeHTTP(writer, request)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// enableSentry turns on Sentry error reporting when
|
||||||
|
// DNSWATCHER_SENTRY_DSN is set, and does nothing when it is not. A DSN
|
||||||
|
// that Sentry cannot parse is an error, so that startup stops instead
|
||||||
|
// of running without the error reporting the operator asked for.
|
||||||
|
func (s *Server) enableSentry() error {
|
||||||
|
if s.params.Config.SentryDSN == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := sentry.Init(sentry.ClientOptions{
|
||||||
|
Dsn: s.params.Config.SentryDSN,
|
||||||
|
Release: s.params.Globals.Appname + "-" + s.params.Globals.Version,
|
||||||
|
// Use the transport that queues each report as it is made. With
|
||||||
|
// the default one, Flush can return before sending a report made
|
||||||
|
// just before it, such as one from the last request at shutdown.
|
||||||
|
DisableTelemetryBuffer: true,
|
||||||
|
// Send panic reports only, not Sentry's counts of what it dropped,
|
||||||
|
// such as the transaction it starts for every request.
|
||||||
|
DisableClientReports: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid DNSWATCHER_SENTRY_DSN: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.log.Info("sentry error reporting activated")
|
||||||
|
s.sentryEnabled = true
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// flushSentry sends the error reports Sentry still holds, waiting at
|
||||||
|
// most sentryFlushTimeout.
|
||||||
|
func (s *Server) flushSentry() {
|
||||||
|
if !s.sentryEnabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !sentry.Flush(sentryFlushTimeout) {
|
||||||
|
s.log.Warn("sentry flush timed out; some error reports were not sent")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,18 +17,13 @@ import (
|
|||||||
"sneak.berlin/go/dnswatcher/internal/state"
|
"sneak.berlin/go/dnswatcher/internal/state"
|
||||||
)
|
)
|
||||||
|
|
||||||
// buildServer wires a *server.Server exactly as cmd/dnswatcher does,
|
// newServerApp builds an fx app holding a *server.Server wired exactly
|
||||||
// minus the watcher/resolver subtree that would touch live DNS. fx
|
// as cmd/dnswatcher wires it, minus the watcher/resolver subtree that
|
||||||
// builds the object graph but the lifecycle is never started, so no
|
// would touch live DNS, plus the given option. config.New reads viper,
|
||||||
// OnStart hook runs and nothing listens or resolves. The caller must
|
// so the caller must first configure it, which is also why the caller
|
||||||
// first configure viper (config.New reads it), which is also why the
|
// cannot run in parallel.
|
||||||
// caller cannot run in parallel.
|
func newServerApp(option fx.Option) *fx.App {
|
||||||
func buildServer(t *testing.T) *server.Server {
|
return fx.New(
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
var srv *server.Server
|
|
||||||
|
|
||||||
app := fx.New(
|
|
||||||
fx.NopLogger,
|
fx.NopLogger,
|
||||||
fx.Provide(
|
fx.Provide(
|
||||||
globals.New,
|
globals.New,
|
||||||
@@ -41,8 +36,18 @@ func buildServer(t *testing.T) *server.Server {
|
|||||||
handlers.New,
|
handlers.New,
|
||||||
server.New,
|
server.New,
|
||||||
),
|
),
|
||||||
fx.Populate(&srv),
|
option,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildServer builds the server without starting the app's lifecycle,
|
||||||
|
// so no OnStart hook runs and nothing listens or resolves.
|
||||||
|
func buildServer(t *testing.T) *server.Server {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
var srv *server.Server
|
||||||
|
|
||||||
|
app := newServerApp(fx.Populate(&srv))
|
||||||
|
|
||||||
err := app.Err()
|
err := app.Err()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user