Skip to content

Commit 1ed88e7

Browse files
author
aereal
authored
Merge pull request #26 from aereal/otel
support OpenTelemetry tracing
2 parents 87d8c2b + 721bc0e commit 1ed88e7

File tree

4 files changed

+155
-16
lines changed

4 files changed

+155
-16
lines changed

Diff for: go.mod

+6-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,9 @@ module github.com/aereal/go-openapi3-validation-middleware
22

33
go 1.16
44

5-
require github.com/getkin/kin-openapi v0.122.0
5+
require (
6+
github.com/getkin/kin-openapi v0.122.0
7+
go.opentelemetry.io/otel v1.21.0
8+
go.opentelemetry.io/otel/sdk v1.21.0
9+
go.opentelemetry.io/otel/trace v1.21.0
10+
)

Diff for: go.sum

+19-1
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,20 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
44
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
55
github.com/getkin/kin-openapi v0.122.0 h1:WB9Jbl0Hp/T79/JF9xlSW5Kl9uYdk/AWD0yAd9HOM10=
66
github.com/getkin/kin-openapi v0.122.0/go.mod h1:PCWw/lfBrJY4HcdqE3jj+QFkaFK8ABoqo7PvqVhXXqw=
7+
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
8+
github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
9+
github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
10+
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
11+
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
712
github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
813
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
914
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
1015
github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU=
1116
github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
1217
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
1318
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
19+
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
20+
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
1421
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
1522
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
1623
github.com/invopop/yaml v0.2.0 h1:7zky/qH+O0DwAyoobXUqvVBwgBFRxKoQ/3FjcVpjTMY=
@@ -37,12 +44,23 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE
3744
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
3845
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
3946
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
40-
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
4147
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
48+
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
49+
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
4250
github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo=
4351
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
4452
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
4553
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
54+
go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc=
55+
go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo=
56+
go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4=
57+
go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM=
58+
go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8=
59+
go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E=
60+
go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc=
61+
go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ=
62+
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
63+
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
4664
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
4765
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
4866
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

Diff for: middleware.go

+35-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package openapi3middleware
22

33
import (
4+
"context"
45
"encoding/json"
56
"errors"
67
"fmt"
@@ -9,6 +10,8 @@ import (
910
"github.com/getkin/kin-openapi/openapi3"
1011
"github.com/getkin/kin-openapi/openapi3filter"
1112
"github.com/getkin/kin-openapi/routers"
13+
"go.opentelemetry.io/otel"
14+
"go.opentelemetry.io/otel/trace"
1215
)
1316

1417
type middleware = func(next http.Handler) http.Handler
@@ -19,6 +22,7 @@ type MiddlewareOptions struct {
1922
ReportFindRouteError func(w http.ResponseWriter, r *http.Request, err error)
2023
ReportRequestValidationError func(w http.ResponseWriter, r *http.Request, err error)
2124
ReportResponseValidationError func(w http.ResponseWriter, r *http.Request, err error)
25+
TracerProvider trace.TracerProvider
2226
}
2327

2428
func (o MiddlewareOptions) reportFindRouteError(w http.ResponseWriter, r *http.Request, err error) {
@@ -60,13 +64,18 @@ func WithResponseValidation(options MiddlewareOptions) middleware {
6064
return func(next http.Handler) http.Handler {
6165
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
6266
ctx := r.Context()
67+
ctx, span := getTracer(ctx, options).Start(ctx, "ResponseValidation")
68+
defer span.End()
6369
irw := newBufferingResponseWriter(w)
64-
next.ServeHTTP(irw, r)
70+
next.ServeHTTP(irw, r.WithContext(ctx))
6571
ri, err := buildRequestValidationInputFromRequest(options.Router, r, options.ValidationOptions)
6672
if frErr := new(findRouteErr); errors.As(err, &frErr) {
67-
options.reportFindRouteError(w, r, frErr.Unwrap())
73+
actualErr := frErr.Unwrap()
74+
span.RecordError(actualErr)
75+
options.reportFindRouteError(w, r, actualErr)
6876
return
6977
} else if err != nil {
78+
span.RecordError(err)
7079
respondErrorJSON(w, http.StatusInternalServerError, err)
7180
return
7281
}
@@ -81,6 +90,7 @@ func WithResponseValidation(options MiddlewareOptions) middleware {
8190
bodyBytes := irw.buf.Bytes()
8291
input.SetBodyBytes(bodyBytes)
8392
if err := openapi3filter.ValidateResponse(ctx, input); err != nil {
93+
span.RecordError(err)
8494
options.reportRespError(w, r, err)
8595
return
8696
}
@@ -94,20 +104,26 @@ func WithResponseValidation(options MiddlewareOptions) middleware {
94104
func WithRequestValidation(options MiddlewareOptions) middleware {
95105
return func(next http.Handler) http.Handler {
96106
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
107+
ctx := r.Context()
108+
ctx, span := getTracer(ctx, options).Start(ctx, "RequestValidation")
109+
defer span.End()
97110
input, err := buildRequestValidationInputFromRequest(options.Router, r, options.ValidationOptions)
98111
if frErr := new(findRouteErr); errors.As(err, &frErr) {
99-
options.reportFindRouteError(w, r, frErr.Unwrap())
112+
actualErr := frErr.Unwrap()
113+
span.RecordError(actualErr)
114+
options.reportFindRouteError(w, r, actualErr)
100115
return
101116
} else if err != nil {
117+
span.RecordError(err)
102118
respondErrorJSON(w, http.StatusInternalServerError, err)
103119
return
104120
}
105-
ctx := r.Context()
106121
if err := openapi3filter.ValidateRequest(ctx, input); err != nil {
122+
span.RecordError(err)
107123
options.reportReqError(w, r, err)
108124
return
109125
}
110-
next.ServeHTTP(w, r)
126+
next.ServeHTTP(w, r.WithContext(ctx))
111127
})
112128
}
113129
}
@@ -218,3 +234,17 @@ func respondJSON(w http.ResponseWriter, statusCode int, payload interface{}) err
218234
w.WriteHeader(statusCode)
219235
return json.NewEncoder(w).Encode(payload)
220236
}
237+
238+
const tracerName = "github.com/aereal/go-openapi3-validation-middleware"
239+
240+
func getTracer(ctx context.Context, opts MiddlewareOptions) trace.Tracer {
241+
tp := opts.TracerProvider
242+
if tp == nil {
243+
if span := trace.SpanFromContext(ctx); span.SpanContext().IsValid() {
244+
tp = span.TracerProvider()
245+
} else {
246+
tp = otel.GetTracerProvider()
247+
}
248+
}
249+
return tp.Tracer(tracerName)
250+
}

Diff for: middleware_test.go

+95-9
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package openapi3middleware
33
import (
44
"bufio"
55
"bytes"
6+
"context"
67
"encoding/json"
78
"errors"
89
"fmt"
@@ -20,24 +21,33 @@ import (
2021
"github.com/getkin/kin-openapi/openapi3filter"
2122
"github.com/getkin/kin-openapi/routers"
2223
"github.com/getkin/kin-openapi/routers/gorillamux"
24+
"go.opentelemetry.io/otel"
25+
"go.opentelemetry.io/otel/propagation"
26+
sdktrace "go.opentelemetry.io/otel/sdk/trace"
27+
"go.opentelemetry.io/otel/sdk/trace/tracetest"
28+
"go.opentelemetry.io/otel/trace"
2329
)
2430

25-
type user struct {
26-
Name string `json:"name"`
27-
ID string `json:"id"`
28-
Age int `json:"age"`
29-
}
31+
var router routers.Router
3032

31-
func TestWithValidation(t *testing.T) {
33+
func init() {
3234
doc, err := openapi3.NewLoader().LoadFromFile("./testdata/user-account-service.openapi.json")
3335
if err != nil {
34-
t.Fatal(err)
36+
panic(err)
3537
}
36-
router, err := gorillamux.NewRouter(doc)
38+
router, err = gorillamux.NewRouter(doc)
3739
if err != nil {
38-
t.Fatal(err)
40+
panic(err)
3941
}
42+
}
4043

44+
type user struct {
45+
Name string `json:"name"`
46+
ID string `json:"id"`
47+
Age int `json:"age"`
48+
}
49+
50+
func TestWithValidation(t *testing.T) {
4151
testCases := []struct {
4252
name string
4353
handler http.Handler
@@ -178,6 +188,82 @@ func TestWithValidation(t *testing.T) {
178188
}
179189
}
180190

191+
func TestWithValidation_otel(t *testing.T) {
192+
testCases := []struct {
193+
name string
194+
buildOptions func(tp trace.TracerProvider) MiddlewareOptions
195+
wantSpans int
196+
}{
197+
{
198+
name: "ok/explicitly passing TracerProvider",
199+
buildOptions: func(tp trace.TracerProvider) MiddlewareOptions {
200+
return MiddlewareOptions{
201+
Router: router,
202+
TracerProvider: tp,
203+
}
204+
},
205+
wantSpans: 3,
206+
},
207+
{
208+
name: "ok/use TracerProvider comes from the current span",
209+
buildOptions: func(_ trace.TracerProvider) MiddlewareOptions {
210+
return MiddlewareOptions{Router: router}
211+
},
212+
wantSpans: 3,
213+
},
214+
}
215+
for _, tc := range testCases {
216+
tc := tc
217+
t.Run(tc.name, func(t *testing.T) {
218+
ctx, cancel := context.WithCancel(context.Background())
219+
if deadline, ok := t.Deadline(); ok {
220+
ctx, cancel = context.WithDeadline(ctx, deadline)
221+
}
222+
defer cancel()
223+
224+
exporter := tracetest.NewInMemoryExporter()
225+
tp := sdktrace.NewTracerProvider(sdktrace.WithBatcher(exporter))
226+
227+
withOtel := func(next http.Handler) http.Handler {
228+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
229+
ctx := otel.GetTextMapPropagator().Extract(r.Context(), propagation.HeaderCarrier(r.Header))
230+
ctx, span := tp.Tracer("test").Start(ctx, fmt.Sprintf("%s %s", r.Method, r.URL.Path))
231+
defer span.End()
232+
next.ServeHTTP(w, r.WithContext(ctx))
233+
})
234+
}
235+
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
236+
w.Header().Set("content-type", "application/json")
237+
_ = json.NewEncoder(w).Encode(user{Name: "aereal", Age: 17, ID: "123"})
238+
})
239+
srv := httptest.NewServer(withOtel(WithValidation(tc.buildOptions(tp))(handler)))
240+
defer srv.Close()
241+
242+
req := mustRequest(newRequest(http.MethodPost, srv.URL+"/users", map[string]string{"content-type": "application/json"}, `{"name":"aereal","age":17}`))
243+
resp, err := srv.Client().Do(req.WithContext(ctx))
244+
if err != nil {
245+
t.Fatalf("http.Client.Do: %v", err)
246+
}
247+
defer resp.Body.Close()
248+
if resp.StatusCode != http.StatusOK {
249+
t.Errorf("unexpected status code: %d", resp.StatusCode)
250+
}
251+
252+
if err := tp.ForceFlush(ctx); err != nil {
253+
t.Fatal(err)
254+
}
255+
spans := exporter.GetSpans()
256+
t.Logf("%d spans got", len(spans))
257+
for i, span := range spans {
258+
t.Logf("#%d: %#v", i, span)
259+
}
260+
if len(spans) != tc.wantSpans {
261+
t.Errorf("spans count:\nwant: %d\ngot: %d", tc.wantSpans, len(spans))
262+
}
263+
})
264+
}
265+
}
266+
181267
func resumeResponse(testName string, got *http.Response) (*http.Response, error) {
182268
imported, err := importResponse(testName)
183269
if err == nil {

0 commit comments

Comments
 (0)