Compare commits

..
1 Commits
Author SHA1 Message Date
renovate 808832cb8e fix(deps): update opentelemetry
otelsetup / test (push) Skipped
otelsetup / vulnerabilities (push) Skipped
pre-commit / pre-commit (push) Skipped
renovate/stability-days Updates have met minimum release age requirement
otelsetup / vulnerabilities (pull_request) Successful in 51s
otelsetup / test (pull_request) Successful in 1m5s
pre-commit / pre-commit (pull_request) Failing after 2m43s
2026-10-05 11:12:59 +00:00
14 changed files with 29 additions and 221 deletions

No files matched your search

+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
go-version: stable
- uses: actions/setup-python@v7
with:
python-version: '3.15'
python-version: '3.14'
- name: Install goimports
run: go install golang.org/x/tools/cmd/goimports@latest
- uses: pre-commit/action@v3.0.1
+1 -1
View File
@@ -23,7 +23,7 @@ repos:
- id: go-imports
args:
- -local
- git.unbound.se/shiny/otelsetup
- gitea.unbound.se/shiny/otelsetup
- repo: https://github.com/lietu/go-pre-commit
rev: v1.0.0
hooks:
+1 -1
View File
@@ -1,3 +1,3 @@
{
"version": "v0.8.0"
"version": "v0.6.1"
}
-35
View File
@@ -2,41 +2,6 @@
All notable changes to this project will be documented in this file.
## [0.8.0] - 2026-10-17
### 🚀 Features
- Move module path to git.unbound.se (#212)
### 🐛 Bug Fixes
- *(deps)* Update module github.com/vektah/gqlparser/v2 to v2.5.62 (#209)
### ⚙️ Miscellaneous Tasks
- *(renovate)* Drop the OpenTelemetry group, now in the central config (#208)
## [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
### 🐛 Bug Fixes
+1 -9
View File
@@ -17,7 +17,7 @@ Provides standardized OpenTelemetry configuration for tracing, metrics, and logg
### Usage
```go
import "git.unbound.se/shiny/otelsetup"
import "gitea.unbound.se/shiny/otelsetup"
// Bootstrap OpenTelemetry SDK
shutdown, err := otelsetup.SetupOTelSDK(ctx, enabled, serviceName, buildVersion, environment)
@@ -28,12 +28,6 @@ defer shutdown(ctx)
// Wrap HTTP handlers with tracing
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
@@ -43,8 +37,6 @@ srv.SetRecoverFunc(otelsetup.Recover)
- **Logging** - Stdout log exporter
- **Propagation** - TraceContext and Baggage propagation
- **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
+5 -5
View File
@@ -1,16 +1,16 @@
module git.unbound.se/shiny/otelsetup
module gitea.unbound.se/shiny/otelsetup
go 1.26.0
require (
codeberg.org/eventsourced/eventsourced v1.24.0
codeberg.org/eventsourced/pg/v2 v2.3.0
codeberg.org/eventsourced/pg/v2 v2.2.1
github.com/99designs/gqlgen v0.17.95
github.com/vektah/gqlparser/v2 v2.5.62
go.opentelemetry.io/otel v1.47.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.47.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.23.0
go.opentelemetry.io/otel/log v1.47.0
go.opentelemetry.io/otel/metric v1.47.0
go.opentelemetry.io/otel/sdk v1.47.0
go.opentelemetry.io/otel/sdk/log v1.47.0
@@ -31,12 +31,12 @@ require (
github.com/pressly/goose/v3 v3.28.0 // indirect
github.com/sethvargo/go-retry v0.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/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.uber.org/multierr v1.11.0 // indirect
golang.org/x/net v0.60.0 // indirect
golang.org/x/net v0.59.0 // indirect
golang.org/x/sync v0.23.0 // indirect
golang.org/x/sys v0.48.0 // indirect
golang.org/x/text v0.42.0 // indirect
+6 -6
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/go.mod h1:uy0HxejDdNFpI+Yp7VOEvjfEUFJQX1VkCZ4e4JyDp7Q=
codeberg.org/eventsourced/pg/v2 v2.3.0 h1:g6kZ9DASeFrC8C+bDcXzbYD8409KHzWmyLOI5sCdNWI=
codeberg.org/eventsourced/pg/v2 v2.3.0/go.mod h1:qPxl3382m7lbhbrEaxwSZ9PrAPoODj2+TqnV3tIKWzU=
codeberg.org/eventsourced/pg/v2 v2.2.1 h1:WAZg40cX+lxwy3/NvJpxjG3am9mpQucywMmi9IcdOF4=
codeberg.org/eventsourced/pg/v2 v2.2.1/go.mod h1:qPxl3382m7lbhbrEaxwSZ9PrAPoODj2+TqnV3tIKWzU=
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/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
@@ -58,8 +58,8 @@ github.com/sosodev/duration v1.4.0 h1:35ed0KiVFriGHHzZZJaZLgmTEEICIyt8Sx0RQfj9Ij
github.com/sosodev/duration v1.4.0/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
github.com/vektah/gqlparser/v2 v2.5.62 h1:GJP/emj1wIWKs0U3SHcAgzknWgylTqQ1qH2gNL+wSWc=
github.com/vektah/gqlparser/v2 v2.5.62/go.mod h1:JNK+plRwKdXLsF/qPFPe5tE0z4s1WeroD9S5LR8um/Q=
github.com/vektah/gqlparser/v2 v2.5.37 h1:jbb1Ilv+xBklV6653tKb4oVUupPNTLb5LmrnBKVI12Y=
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/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.47.0 h1:j7ALJ/zgkS7Z6aeJW09p8VC9804bC+PpeTfCD4XPnOM=
@@ -96,8 +96,8 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
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/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU=
golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=
golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
-1
View File
@@ -13,7 +13,6 @@ import (
// eventsourcedMeterName is the instrumentation scope for the event-sourcing
// metrics emitted by the adapter returned from NewEventsourcedMetrics.
// It keeps the old module path on purpose: renaming it breaks queries on otel_scope_name.
const eventsourcedMeterName = "gitea.unbound.se/shiny/otelsetup/eventsourced"
// durationBucketsSeconds are explicit histogram boundaries tuned for
+2 -1
View File
@@ -13,6 +13,7 @@ import (
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
"go.opentelemetry.io/otel/exporters/stdout/stdoutlog"
"go.opentelemetry.io/otel/log/global"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/log"
"go.opentelemetry.io/otel/sdk/metric"
@@ -85,7 +86,7 @@ func SetupOTelSDK(ctx context.Context, enabled bool, serviceName, buildVersion,
processor := log.NewSimpleProcessor(logExporter)
logProvider := log.NewLoggerProvider(log.WithProcessor(processor))
otel.SetLoggerProvider(logProvider)
global.SetLoggerProvider(logProvider)
shutdownFuncs = append(shutdownFuncs, logProvider.Shutdown)
exp, err := otlpmetrichttp.New(ctx, otlpmetrichttp.WithHTTPClient(otlpHTTPClient()))
-41
View File
@@ -1,41 +0,0 @@
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
@@ -1,117 +0,0 @@
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")
}
})
}
}
+10
View File
@@ -2,5 +2,15 @@
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended"
],
"packageRules": [
{
"groupName": "OpenTelemetry",
"matchPackageNames": [
"go.opentelemetry.io/**",
"gitea.unbound.se/shiny/otelsetup",
"gitea.unbound.se/shiny/logging"
]
}
]
}
+1 -2
View File
@@ -10,7 +10,6 @@ import (
// subscriptionMeterName is the instrumentation scope for the cross-service
// subscription push metrics emitted by SubscriptionMetrics.
// It keeps the old module path on purpose: renaming it breaks queries on otel_scope_name.
const subscriptionMeterName = "gitea.unbound.se/shiny/otelsetup/subscriptions"
// SubscriptionMetrics records cross-service subscription push outcomes (the
@@ -18,7 +17,7 @@ const subscriptionMeterName = "gitea.unbound.se/shiny/otelsetup/subscriptions"
// counters.
//
// Its method set (Pushed/PushSkipped/Dropped/ChannelFull) satisfies the
// Observer interface of git.unbound.se/shiny/subscriptions *structurally* —
// Observer interface of gitea.unbound.se/shiny/subscriptions *structurally* —
// otelsetup does not import that library. A service constructs it and passes it
// to the registry:
//
+1 -1
View File
@@ -11,7 +11,7 @@ import (
"go.opentelemetry.io/otel/sdk/metric/metricdata"
)
// observerShape mirrors git.unbound.se/shiny/subscriptions.Observer. This
// observerShape mirrors gitea.unbound.se/shiny/subscriptions.Observer. This
// compile-time assertion guards that SubscriptionMetrics still satisfies that
// interface structurally, without otelsetup importing the library.
//