Compare commits

...
6 Commits
Author SHA1 Message Date
releaser 12721fa6d7 chore(release): prepare for v0.7.0 (#207)
Unbound Release / Create Tag (push) Skipped
otelsetup / vulnerabilities (push) Successful in 29s
Unbound Release / Check Preconditions (push) Successful in 1s
otelsetup / test (push) Successful in 37s
Release / release (push) Successful in 11s
Unbound Release / Create Release (push) Successful in 4s
Unbound Release / Generate Changelog and Handle PR (push) Successful in 5s
pre-commit / pre-commit (push) Successful in 2m24s
## [0.7.0] - 2026-10-16

### 🚀 Features

- Add gqlgen Recover that logs panics as one structured record (#206)

### 🐛 Bug Fixes

- *(deps)* Update module codeberg.org/eventsourced/pg/v2 to v2.2.0 (#186)
- *(deps)* Update module codeberg.org/eventsourced/pg/v2 to v2.2.1 (#188)
- *(deps)* Update module codeberg.org/eventsourced/pg/v2 to v2.3.0 (#200)
- *(deps)* Update opentelemetry (#198)
- *(deps)* Update golang.org/x/net to v0.60.0 and golang.org/x/tools to v0.51.0 (#203)

### ⚙️ Miscellaneous Tasks

- *(deps)* Update pre-commit hook alessandrojcm/commitlint-pre-commit-hook to v9.27.0 (#190)
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.14.0 (#192)
- *(renovate)* Group OpenTelemetry, otelsetup and logging updates (#196)
- *(deps)* Update dependency python to 3.15 (#204)

<!-- generated by git-cliff -->

---

**Note:** Please use **Squash Merge** when merging this PR.

Reviewed-on: #207
Co-authored-by: Unbound Releaser <releaser@unbound.se>
2026-10-16 23:17:22 +00:00
argoyle 17130005c9 feat: add gqlgen Recover that logs panics as one structured record (#206)
otelsetup / test (push) Successful in 43s
Unbound Release / Create Release (push) Successful in 4s
Unbound Release / Check Preconditions (push) Successful in 1s
otelsetup / vulnerabilities (push) Successful in 44s
Unbound Release / Create Tag (push) Skipped
Unbound Release / Generate Changelog and Handle PR (push) Successful in 9s
Release / release (push) Successful in 14s
pre-commit / pre-commit (push) Successful in 2m25s
## Summary

gqlgen's `graphql.DefaultRecover` prints a recovered resolver panic and `debug.PrintStack()` to stderr, so the log collector stores every stack line as a separate, level-less entry (~60 entries per panic in production).

Adds `otelsetup.Recover` (`func(ctx context.Context, err any) error`, a `graphql.RecoverFunc` for gqlgen v0.17.95):

- logs the panic as **one** ERROR record via `slog.Default().ErrorContext` (the default logger services set up through `logging.SetupLogger`), with `panic`, `stack` (`debug.Stack()`) and `path` (GraphQL field path, when available) attributes
- records the error on the active span (`RecordError` with stack trace, `SetStatus(codes.Error)`)
- returns the generic `internal system error` gqlerror to the client, without the panic text

Services install it with `srv.SetRecoverFunc(otelsetup.Recover)`.

## Test plan

- `TestRecover`: error and non-error panic values; asserts the generic gqlerror, no panic text leak, exactly one ERROR log record with panic/stack attributes, and span status Error with an `exception` event carrying message and stacktrace (in-memory `tracetest.SpanRecorder`)
- `go test -race ./...` passes, `prek run --all-files` passes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01H6eVuR4Yt3m7TbDTDwyZJi
Reviewed-on: #206
2026-10-16 23:12:44 +00:00
renovate 9fe1a21c02 chore(deps): update dependency python to 3.15 (#204)
Unbound Release / Generate Changelog and Handle PR (push) Successful in 12s
Unbound Release / Create Release (push) Successful in 6s
Release / release (push) Successful in 21m54s
otelsetup / test (push) Successful in 40s
Unbound Release / Create Tag (push) Skipped
Unbound Release / Check Preconditions (push) Successful in 1s
otelsetup / vulnerabilities (push) Successful in 33s
pre-commit / pre-commit (push) Successful in 1m10s
2026-10-13 21:00:45 +00:00
argoyle 63a04e8044 fix(deps): update golang.org/x/net to v0.60.0 and golang.org/x/tools to v0.51.0 (#203)
Unbound Release / Create Release (push) Canceled after 0s
Unbound Release / Generate Changelog and Handle PR (push) Canceled after 0s
Release / release (push) Canceled after 29h34m15s
otelsetup / test (push) Successful in 14m49s
otelsetup / vulnerabilities (push) Successful in 12m17s
Unbound Release / Check Preconditions (push) Successful in 5m18s
Unbound Release / Create Tag (push) Skipped
pre-commit / pre-commit (push) Failing after 24m13s
2026-10-11 06:56:54 +00:00
renovate ffaf6a7f27 fix(deps): update opentelemetry (#198)
Unbound Release / Check Preconditions (push) Successful in 20s
Unbound Release / Create Tag (push) Skipped
otelsetup / test (push) Skipped
otelsetup / vulnerabilities (push) Skipped
pre-commit / pre-commit (push) Skipped
Unbound Release / Generate Changelog and Handle PR (push) Successful in 40s
Release / release (push) Successful in 17m53s
Unbound Release / Create Release (push) Successful in 24s
2026-10-05 18:16:37 +00:00
renovate a16f09de4a fix(deps): update module codeberg.org/eventsourced/pg/v2 to v2.3.0 (#200)
Unbound Release / Check Preconditions (push) Successful in 29s
Unbound Release / Create Tag (push) Skipped
Release / release (push) Successful in 4m17s
otelsetup / test (push) Skipped
otelsetup / vulnerabilities (push) Skipped
pre-commit / pre-commit (push) Skipped
Unbound Release / Generate Changelog and Handle PR (push) Successful in 44s
Unbound Release / Create Release (push) Successful in 33s
2026-10-05 17:24:43 +00:00
9 changed files with 249 additions and 63 deletions

No files matched your search

+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
go-version: stable go-version: stable
- uses: actions/setup-python@v7 - uses: actions/setup-python@v7
with: with:
python-version: '3.14' python-version: '3.15'
- name: Install goimports - name: Install goimports
run: go install golang.org/x/tools/cmd/goimports@latest run: go install golang.org/x/tools/cmd/goimports@latest
- uses: pre-commit/action@v3.0.1 - uses: pre-commit/action@v3.0.1
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"version": "v0.6.1" "version": "v0.7.0"
} }
+21
View File
@@ -2,6 +2,27 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## [0.7.0] - 2026-10-16
### 🚀 Features
- Add gqlgen Recover that logs panics as one structured record (#206)
### 🐛 Bug Fixes
- *(deps)* Update module codeberg.org/eventsourced/pg/v2 to v2.2.0 (#186)
- *(deps)* Update module codeberg.org/eventsourced/pg/v2 to v2.2.1 (#188)
- *(deps)* Update module codeberg.org/eventsourced/pg/v2 to v2.3.0 (#200)
- *(deps)* Update opentelemetry (#198)
- *(deps)* Update golang.org/x/net to v0.60.0 and golang.org/x/tools to v0.51.0 (#203)
### ⚙️ Miscellaneous Tasks
- *(deps)* Update pre-commit hook alessandrojcm/commitlint-pre-commit-hook to v9.27.0 (#190)
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.14.0 (#192)
- *(renovate)* Group OpenTelemetry, otelsetup and logging updates (#196)
- *(deps)* Update dependency python to 3.15 (#204)
## [0.6.1] - 2026-09-19 ## [0.6.1] - 2026-09-19
### 🐛 Bug Fixes ### 🐛 Bug Fixes
+8
View File
@@ -28,6 +28,12 @@ defer shutdown(ctx)
// Wrap HTTP handlers with tracing // Wrap HTTP handlers with tracing
http.Handle("/", otelsetup.Handler(myHandler)) http.Handle("/", otelsetup.Handler(myHandler))
// gqlgen server hooks
srv.AroundOperations(otelsetup.AroundOperations)
srv.AroundRootFields(otelsetup.AroundRootFields)
srv.AroundFields(otelsetup.AroundFields)
srv.SetRecoverFunc(otelsetup.Recover)
``` ```
### Features ### Features
@@ -37,6 +43,8 @@ http.Handle("/", otelsetup.Handler(myHandler))
- **Logging** - Stdout log exporter - **Logging** - Stdout log exporter
- **Propagation** - TraceContext and Baggage propagation - **Propagation** - TraceContext and Baggage propagation
- **HTTP Handler** - Middleware for automatic span creation - **HTTP Handler** - Middleware for automatic span creation
- **gqlgen hooks** - Spans per operation, root field and resolver field
- **gqlgen recover** - `Recover` logs a resolver panic as one ERROR record via `slog.Default()` (attributes `panic`, `stack`, `path`), records it on the active span with status Error, and returns the generic "internal system error" to the client (gqlgen's default prints the stack to stderr line by line)
### Environment Variables ### Environment Variables
+19 -19
View File
@@ -4,18 +4,18 @@ go 1.26.0
require ( require (
codeberg.org/eventsourced/eventsourced v1.24.0 codeberg.org/eventsourced/eventsourced v1.24.0
codeberg.org/eventsourced/pg/v2 v2.2.1 codeberg.org/eventsourced/pg/v2 v2.3.0
github.com/99designs/gqlgen v0.17.95 github.com/99designs/gqlgen v0.17.95
go.opentelemetry.io/otel v1.46.0 github.com/vektah/gqlparser/v2 v2.5.37
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.46.0 go.opentelemetry.io/otel v1.47.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.46.0 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.47.0
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.22.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0
go.opentelemetry.io/otel/log v0.22.0 go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.23.0
go.opentelemetry.io/otel/metric v1.46.0 go.opentelemetry.io/otel/metric v1.47.0
go.opentelemetry.io/otel/sdk v1.46.0 go.opentelemetry.io/otel/sdk v1.47.0
go.opentelemetry.io/otel/sdk/log v0.22.0 go.opentelemetry.io/otel/sdk/log v1.47.0
go.opentelemetry.io/otel/sdk/metric v1.46.0 go.opentelemetry.io/otel/sdk/metric v1.47.0
go.opentelemetry.io/otel/trace v1.46.0 go.opentelemetry.io/otel/trace v1.47.0
) )
require ( require (
@@ -31,17 +31,17 @@ require (
github.com/pressly/goose/v3 v3.28.0 // indirect github.com/pressly/goose/v3 v3.28.0 // indirect
github.com/sethvargo/go-retry v0.4.0 // indirect github.com/sethvargo/go-retry v0.4.0 // indirect
github.com/sosodev/duration v1.4.0 // indirect github.com/sosodev/duration v1.4.0 // indirect
github.com/vektah/gqlparser/v2 v2.5.37 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0 // indirect
go.opentelemetry.io/otel/log v1.47.0 // indirect
go.opentelemetry.io/proto/otlp v1.11.0 // indirect go.opentelemetry.io/proto/otlp v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
golang.org/x/net v0.58.0 // indirect golang.org/x/net v0.60.0 // indirect
golang.org/x/sync v0.22.0 // indirect golang.org/x/sync v0.23.0 // indirect
golang.org/x/sys v0.47.0 // indirect golang.org/x/sys v0.48.0 // indirect
golang.org/x/text v0.41.0 // indirect golang.org/x/text v0.42.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260819154853-08b0e4226688 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260831171406-18b4a7587f8a // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc // indirect
google.golang.org/grpc v1.83.2 // indirect google.golang.org/grpc v1.83.2 // indirect
google.golang.org/protobuf v1.36.12 // indirect google.golang.org/protobuf v1.36.12 // indirect
) )
+40 -40
View File
@@ -1,7 +1,7 @@
codeberg.org/eventsourced/eventsourced v1.24.0 h1:yNB+74JNB/NHo7D8ZR/yQcTVDYrWnjFf0xUKLgViqRc= codeberg.org/eventsourced/eventsourced v1.24.0 h1:yNB+74JNB/NHo7D8ZR/yQcTVDYrWnjFf0xUKLgViqRc=
codeberg.org/eventsourced/eventsourced v1.24.0/go.mod h1:uy0HxejDdNFpI+Yp7VOEvjfEUFJQX1VkCZ4e4JyDp7Q= codeberg.org/eventsourced/eventsourced v1.24.0/go.mod h1:uy0HxejDdNFpI+Yp7VOEvjfEUFJQX1VkCZ4e4JyDp7Q=
codeberg.org/eventsourced/pg/v2 v2.2.1 h1:WAZg40cX+lxwy3/NvJpxjG3am9mpQucywMmi9IcdOF4= codeberg.org/eventsourced/pg/v2 v2.3.0 h1:g6kZ9DASeFrC8C+bDcXzbYD8409KHzWmyLOI5sCdNWI=
codeberg.org/eventsourced/pg/v2 v2.2.1/go.mod h1:qPxl3382m7lbhbrEaxwSZ9PrAPoODj2+TqnV3tIKWzU= codeberg.org/eventsourced/pg/v2 v2.3.0/go.mod h1:qPxl3382m7lbhbrEaxwSZ9PrAPoODj2+TqnV3tIKWzU=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
@@ -62,32 +62,32 @@ github.com/vektah/gqlparser/v2 v2.5.37 h1:jbb1Ilv+xBklV6653tKb4oVUupPNTLb5LmrnBK
github.com/vektah/gqlparser/v2 v2.5.37/go.mod h1:9O4Ox6Ngd3Y12bMD3w6i3CRQXh8W1oC1q0m6olCymDM= github.com/vektah/gqlparser/v2 v2.5.37/go.mod h1:9O4Ox6Ngd3Y12bMD3w6i3CRQXh8W1oC1q0m6olCymDM=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.46.0 h1:FHt5/CDyVxi/8IM1CH7VE/rRgq3kLHa2mSTVMO8AWyc= go.opentelemetry.io/otel v1.47.0 h1:j7ALJ/zgkS7Z6aeJW09p8VC9804bC+PpeTfCD4XPnOM=
go.opentelemetry.io/otel v1.46.0/go.mod h1:Gj3SEScelsNC45tp4nSxRYlS+f5iez7W8XPMCt905kE= go.opentelemetry.io/otel v1.47.0/go.mod h1:8wS9O2qfXrYrzp6hIF/HOYJJf/wIhFPhR2xLuP+iXQU=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.46.0 h1:AP23h/mFgb/lc7tdck1Kfn9qxsM8TAeNPCU5C3pzaps= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.47.0 h1:7kszRN4Ze5GeAkUnBjBSUIunVBMA+mZIG04auueqguM=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.46.0/go.mod h1:K4EqCe1b4kGk5WR690ntg9LaBfsPoV32FwthbyoptuA= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.47.0/go.mod h1:CLzakYy8jOdkBfcveCWYkeKJxujcF4xPhh2Z7IfE98U=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0 h1:OFnwLJr+pF3iHrlGSzbxyuo6/6HyBlnlN1CWEJmBVcw= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0 h1:julhjPeUH/q/7hinbSdDdqt5h7Zw9YWmRlWRhI0jd54=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0/go.mod h1:716wFneO0ov19A2beH5hjfh9AK5z/VWNAtDijp1Y0/g= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0/go.mod h1:Ao2mz688LH/tFf0yMAenidq6k2YNSx6SIY2q6jDACck=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.46.0 h1:KrC1YrQeSt46ITMWAbgQx1M1eV1/1TKzttrBzymPmss= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0 h1:aXZXsZ012wOgVkqaiQv+Z/d8V0xKjmg70F6m4CE94lc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.46.0/go.mod h1:zDSEzoEqsOrgBeGvH66KRgxh90VonFyJqBHA0Pk3+rM= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0/go.mod h1:ziu1gUIJhAaxM3EV1e3Ralk5AAyL1UOYz3lJJ4VSI38=
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.22.0 h1:kvMAiLEudKmk+CSG+iYbU8vTUGNNDaf/V09OO5lrTwI= go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.23.0 h1:PW/E9Tw6ZykC88pey0MZY8hKOUeEw+hX6PU+oXA4A9o=
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.22.0/go.mod h1:L9Dlksri+MdT1cb2gIiA1cJJYW3Y92ipvDjNxYEyaDI= go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.23.0/go.mod h1:hUw4VNfiOnQ/yzGgy6ZdWVnudXw7iRfImhd6FjXOnvI=
go.opentelemetry.io/otel/log v0.22.0 h1:5DBNnfvaJ6CVdkJ+Jle8Tzs50aSSv49TXGj9XRsEYw0= go.opentelemetry.io/otel/log v1.47.0 h1:cOTS1CcLbSQeZKanGJ+0JpF/+t4PELi3O3bbl2lqCcI=
go.opentelemetry.io/otel/log v0.22.0/go.mod h1:gzOt/R67vF2GniAqWu8Qv0SXy89f71muHcrkz76PCdc= go.opentelemetry.io/otel/log v1.47.0/go.mod h1:9byitSQ5pLC6PpqwGXjqdMKya6ZTswHRZh2vvXT33nw=
go.opentelemetry.io/otel/metric v1.46.0 h1:yBnkXvgV7AXFILZc5K6IZe/CBFF3OS7BJ8ov6/lj0K8= go.opentelemetry.io/otel/metric v1.47.0 h1:4PptaldXx3Eat1XjMZ68pPJEs5wrhlemctZE9a3UdWY=
go.opentelemetry.io/otel/metric v1.46.0/go.mod h1:iPmdWqifKUdzziPkvvzIJXITl56fQx2mGM/DHLB3/2o= go.opentelemetry.io/otel/metric v1.47.0/go.mod h1:ADGSXxRrXM6bjbvLo535EstVFlPpPYZm4LBKixjDHwU=
go.opentelemetry.io/otel/metric/x v0.68.0 h1:TA/cBT23D3MnxYPwHL7YFOdYGdx0A0v+s7Mzotpd1dU= go.opentelemetry.io/otel/metric/x v0.69.0 h1:DjRLr15H83v+hCW7JA9NoJvOkYTtmq5YoDRbe9deYpM=
go.opentelemetry.io/otel/metric/x v0.68.0/go.mod h1:agudOmvWhwUTjgibWDzxD2PoWYnpw5Ht5jISYOD2Hd4= go.opentelemetry.io/otel/metric/x v0.69.0/go.mod h1:uVvsMPMFFyj/HUQfrUnH3JjnOQ1dwFDorgFLRBasM0k=
go.opentelemetry.io/otel/sdk v1.46.0 h1:h5CNQQjEbuQXY/JfZtgt3i7HVFV3aHPO2OAwO2eTYPI= go.opentelemetry.io/otel/sdk v1.47.0 h1:zWXEr4j2lFefG87TU6Yg8a7ngfohIKFZHKp0Hf5hC6I=
go.opentelemetry.io/otel/sdk v1.46.0/go.mod h1:GAERFXFt5SYCEB+YiKUbMBeza6UaDH7GmGOZEfh2gSM= go.opentelemetry.io/otel/sdk v1.47.0/go.mod h1:VUc24kiOeoGsxG8G9ULx3fWKvB7jMhnGE8Oi607lgR0=
go.opentelemetry.io/otel/sdk/log v0.22.0 h1:PRL+s6P63XT4E/bheEflopPUpVxuvANqZwtt89yhoGk= go.opentelemetry.io/otel/sdk/log v1.47.0 h1:W4N/ZfgpkRRnmARnqYoQ6BrWA0mrj8IzVDNLlpmUJZY=
go.opentelemetry.io/otel/sdk/log v0.22.0/go.mod h1:JNp0sBELrjCTcu5W3GzABVypeU6vDJjBS+X0JISuz+g= go.opentelemetry.io/otel/sdk/log v1.47.0/go.mod h1:Wu74Z2EmOKXi2FleSNAb0Hyau2G+r5kubSEMJw5aAMs=
go.opentelemetry.io/otel/sdk/log/logtest v0.22.0 h1:infPnfNrhCNgOUZRs3gWUg8vhoBUHihq02gwK05gzlg= go.opentelemetry.io/otel/sdk/log/logtest v0.23.0 h1:j3pm42QCrKYYLLkI6OrV1QHFkUfDA6/4EP+Y1PzggLg=
go.opentelemetry.io/otel/sdk/log/logtest v0.22.0/go.mod h1:gkQZA3z15Bv3KU9vigBTi8dFechSozRP7v94X4VZv+s= go.opentelemetry.io/otel/sdk/log/logtest v0.23.0/go.mod h1:zt0gEnuTZgDvqvczHwYswz93eqMaIr6upTTsdV/HUlU=
go.opentelemetry.io/otel/sdk/metric v1.46.0 h1:0piZ26EG4RBfebb2jhDH6ERCYHoVWduc3kLgPCwSnSE= go.opentelemetry.io/otel/sdk/metric v1.47.0 h1:lfISg2j93VT6yqdk9OfUaZmw/GfcZqCCV3jdXtsPnKw=
go.opentelemetry.io/otel/sdk/metric v1.46.0/go.mod h1:I1PbKrdVc8Qu8HYVDNtqVIwLwjNrhsV/uFuxfwg8mO4= go.opentelemetry.io/otel/sdk/metric v1.47.0/go.mod h1:ypLp+mW1Nt2x+Szt3b5/i1syodyts49lMOwxpDI3VGw=
go.opentelemetry.io/otel/trace v1.46.0 h1:OULy7ccdJnZtJ0UDYFOIGaCmiWzJ8Vi2G/Rsu60qs1c= go.opentelemetry.io/otel/trace v1.47.0 h1:JOjX/Oci8K94QHddo+bbfya/Ai/nf6/dt9ZfrFNWSrM=
go.opentelemetry.io/otel/trace v1.46.0/go.mod h1:J7GAXweO77XSFkB/rmAqk9D6ihszhFjLU+d9WuUxDLI= go.opentelemetry.io/otel/trace v1.47.0/go.mod h1:jNaSLa2PZEYFG6fRjJABAu+bw4FS08uDmPg28lTghu0=
go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk= go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk=
go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E= go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
@@ -96,20 +96,20 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU=
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/api v0.0.0-20260819154853-08b0e4226688 h1:ax2KzoSRIZU/M0cIxri3pKxy99vniH1PVxWC6si/eZI= google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc h1:rZHRz0ogq4Pid1IKtA/ivHi8XONVF5tqUQXA2TtgpkE=
google.golang.org/genproto/googleapis/api v0.0.0-20260819154853-08b0e4226688/go.mod h1:1RJ9BQGyNdZwkGc1eTqkErfRZ6RJyYPHZo73BZ1vQqI= google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc/go.mod h1:K/n0XLazCJHjD73YuMyNJsN38BPPy1kfFnKxUXaThFU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260831171406-18b4a7587f8a h1:3Dnd1cDaZlB68lziofO+bJXpjOy8UfRv8Unt+yH8tQ4= google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc h1:4bNTbnb44EqGVy9HaQxSY2jnafSUdgV3qHtedvNpDKg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260831171406-18b4a7587f8a/go.mod h1:DjtHYE8FKJLivXcBEjGwndXfIC23G0VpXiXKqG179uA= google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc/go.mod h1:OaIUM3+LpYcK2GXM4FTmhWoIq371Owdr+Cc7/BsYHHc=
google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU=
google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8=
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
+1 -2
View File
@@ -13,7 +13,6 @@ import (
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp" "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
"go.opentelemetry.io/otel/exporters/stdout/stdoutlog" "go.opentelemetry.io/otel/exporters/stdout/stdoutlog"
"go.opentelemetry.io/otel/log/global"
"go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/log" "go.opentelemetry.io/otel/sdk/log"
"go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/metric"
@@ -86,7 +85,7 @@ func SetupOTelSDK(ctx context.Context, enabled bool, serviceName, buildVersion,
processor := log.NewSimpleProcessor(logExporter) processor := log.NewSimpleProcessor(logExporter)
logProvider := log.NewLoggerProvider(log.WithProcessor(processor)) logProvider := log.NewLoggerProvider(log.WithProcessor(processor))
global.SetLoggerProvider(logProvider) otel.SetLoggerProvider(logProvider)
shutdownFuncs = append(shutdownFuncs, logProvider.Shutdown) shutdownFuncs = append(shutdownFuncs, logProvider.Shutdown)
exp, err := otlpmetrichttp.New(ctx, otlpmetrichttp.WithHTTPClient(otlpHTTPClient())) exp, err := otlpmetrichttp.New(ctx, otlpmetrichttp.WithHTTPClient(otlpHTTPClient()))
+41
View File
@@ -0,0 +1,41 @@
package otelsetup
import (
"context"
"fmt"
"log/slog"
"runtime/debug"
"github.com/99designs/gqlgen/graphql"
"github.com/vektah/gqlparser/v2/gqlerror"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
)
// Recover is a gqlgen recover function, installed with srv.SetRecoverFunc(otelsetup.Recover).
// Unlike graphql.DefaultRecover, which prints the stack to stderr line by line, it logs the
// panic as a single structured ERROR record (via slog.Default) with the stack as an attribute,
// records it on the active span and marks the span as failed. The client gets the same generic
// "internal system error" as with the default, without the panic text.
func Recover(ctx context.Context, err any) error {
stack := string(debug.Stack())
panicErr, ok := err.(error)
if !ok {
panicErr = fmt.Errorf("%v", err)
}
attrs := []any{
slog.String("panic", panicErr.Error()),
slog.String("stack", stack),
}
if fc := graphql.GetFieldContext(ctx); fc != nil {
attrs = append(attrs, slog.String("path", fc.Path().String()))
}
slog.Default().ErrorContext(ctx, "recovered panic in graphql resolver", attrs...)
span := trace.SpanFromContext(ctx)
span.RecordError(panicErr, trace.WithStackTrace(true))
span.SetStatus(codes.Error, "panic: "+panicErr.Error())
return gqlerror.Errorf("internal system error")
}
+117
View File
@@ -0,0 +1,117 @@
package otelsetup
import (
"context"
"errors"
"log/slog"
"strings"
"sync"
"testing"
"github.com/99designs/gqlgen/graphql"
"github.com/vektah/gqlparser/v2/gqlerror"
"go.opentelemetry.io/otel/codes"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/sdk/trace/tracetest"
)
type recordingHandler struct {
mu sync.Mutex
records []slog.Record
}
func (h *recordingHandler) Enabled(context.Context, slog.Level) bool { return true }
func (h *recordingHandler) Handle(_ context.Context, r slog.Record) error {
h.mu.Lock()
defer h.mu.Unlock()
h.records = append(h.records, r.Clone())
return nil
}
func (h *recordingHandler) WithAttrs([]slog.Attr) slog.Handler { return h }
func (h *recordingHandler) WithGroup(string) slog.Handler { return h }
func TestRecover(t *testing.T) {
var _ graphql.RecoverFunc = Recover
tests := []struct {
name string
panicVal any
wantPanic string
}{
{name: "error value", panicVal: errors.New("runtime error: invalid memory address or nil pointer dereference"), wantPanic: "runtime error: invalid memory address or nil pointer dereference"},
{name: "string value", panicVal: "boom", wantPanic: "boom"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h := &recordingHandler{}
prev := slog.Default()
slog.SetDefault(slog.New(h))
t.Cleanup(func() { slog.SetDefault(prev) })
sr := tracetest.NewSpanRecorder()
tp := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(sr))
ctx, span := tp.Tracer("test").Start(context.Background(), "resolver")
err := Recover(ctx, tt.panicVal)
span.End()
var gqlErr *gqlerror.Error
if !errors.As(err, &gqlErr) || gqlErr.Message != "internal system error" {
t.Fatalf("Recover() error = %v, want gqlerror \"internal system error\"", err)
}
if strings.Contains(err.Error(), tt.wantPanic) {
t.Errorf("Recover() error leaks panic text: %v", err)
}
if len(h.records) != 1 {
t.Fatalf("got %d log records, want exactly 1", len(h.records))
}
rec := h.records[0]
if rec.Level != slog.LevelError {
t.Errorf("log level = %v, want ERROR", rec.Level)
}
got := map[string]string{}
rec.Attrs(func(a slog.Attr) bool {
got[a.Key] = a.Value.String()
return true
})
if got["panic"] != tt.wantPanic {
t.Errorf("panic attr = %q, want %q", got["panic"], tt.wantPanic)
}
if !strings.Contains(got["stack"], "goroutine") {
t.Errorf("stack attr missing stack trace: %q", got["stack"])
}
spans := sr.Ended()
if len(spans) != 1 {
t.Fatalf("got %d spans, want 1", len(spans))
}
s := spans[0]
if s.Status().Code != codes.Error {
t.Errorf("span status = %v, want Error", s.Status().Code)
}
var exception bool
for _, ev := range s.Events() {
if ev.Name != "exception" {
continue
}
exception = true
attrs := map[string]string{}
for _, kv := range ev.Attributes {
attrs[string(kv.Key)] = kv.Value.String()
}
if attrs["exception.message"] != tt.wantPanic {
t.Errorf("exception.message = %q, want %q", attrs["exception.message"], tt.wantPanic)
}
if attrs["exception.stacktrace"] == "" {
t.Error("exception event has no stacktrace")
}
}
if !exception {
t.Error("span has no exception event")
}
})
}
}