...
1 package client
2
3 import (
4 "fmt"
5 "net/http"
6 "strings"
7
8 "github.com/go-openapi/runtime"
9 "github.com/go-openapi/strfmt"
10 "go.opentelemetry.io/otel"
11 "go.opentelemetry.io/otel/attribute"
12 "go.opentelemetry.io/otel/codes"
13 "go.opentelemetry.io/otel/propagation"
14 semconv "go.opentelemetry.io/otel/semconv/v1.17.0"
15 "go.opentelemetry.io/otel/semconv/v1.17.0/httpconv"
16 "go.opentelemetry.io/otel/trace"
17 )
18
19 const (
20 instrumentationVersion = "1.0.0"
21 tracerName = "go-openapi"
22 )
23
24 type config struct {
25 Tracer trace.Tracer
26 Propagator propagation.TextMapPropagator
27 SpanStartOptions []trace.SpanStartOption
28 SpanNameFormatter func(*runtime.ClientOperation) string
29 TracerProvider trace.TracerProvider
30 }
31
32 type OpenTelemetryOpt interface {
33 apply(*config)
34 }
35
36 type optionFunc func(*config)
37
38 func (o optionFunc) apply(c *config) {
39 o(c)
40 }
41
42
43
44 func WithTracerProvider(provider trace.TracerProvider) OpenTelemetryOpt {
45 return optionFunc(func(c *config) {
46 if provider != nil {
47 c.TracerProvider = provider
48 }
49 })
50 }
51
52
53
54 func WithPropagators(ps propagation.TextMapPropagator) OpenTelemetryOpt {
55 return optionFunc(func(c *config) {
56 if ps != nil {
57 c.Propagator = ps
58 }
59 })
60 }
61
62
63
64 func WithSpanOptions(opts ...trace.SpanStartOption) OpenTelemetryOpt {
65 return optionFunc(func(c *config) {
66 c.SpanStartOptions = append(c.SpanStartOptions, opts...)
67 })
68 }
69
70
71
72 func WithSpanNameFormatter(f func(op *runtime.ClientOperation) string) OpenTelemetryOpt {
73 return optionFunc(func(c *config) {
74 c.SpanNameFormatter = f
75 })
76 }
77
78 func defaultTransportFormatter(op *runtime.ClientOperation) string {
79 if op.ID != "" {
80 return op.ID
81 }
82
83 return fmt.Sprintf("%s_%s", strings.ToLower(op.Method), op.PathPattern)
84 }
85
86 type openTelemetryTransport struct {
87 transport runtime.ClientTransport
88 host string
89 tracer trace.Tracer
90 config *config
91 }
92
93 func newOpenTelemetryTransport(transport runtime.ClientTransport, host string, opts []OpenTelemetryOpt) *openTelemetryTransport {
94 tr := &openTelemetryTransport{
95 transport: transport,
96 host: host,
97 }
98
99 defaultOpts := []OpenTelemetryOpt{
100 WithSpanOptions(trace.WithSpanKind(trace.SpanKindClient)),
101 WithSpanNameFormatter(defaultTransportFormatter),
102 WithPropagators(otel.GetTextMapPropagator()),
103 WithTracerProvider(otel.GetTracerProvider()),
104 }
105
106 c := newConfig(append(defaultOpts, opts...)...)
107 tr.config = c
108
109 return tr
110 }
111
112 func (t *openTelemetryTransport) Submit(op *runtime.ClientOperation) (interface{}, error) {
113 if op.Context == nil {
114 return t.transport.Submit(op)
115 }
116
117 params := op.Params
118 reader := op.Reader
119
120 var span trace.Span
121 defer func() {
122 if span != nil {
123 span.End()
124 }
125 }()
126
127 op.Params = runtime.ClientRequestWriterFunc(func(req runtime.ClientRequest, reg strfmt.Registry) error {
128 span = t.newOpenTelemetrySpan(op, req.GetHeaderParams())
129 return params.WriteToRequest(req, reg)
130 })
131
132 op.Reader = runtime.ClientResponseReaderFunc(func(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) {
133 if span != nil {
134 statusCode := response.Code()
135
136 span.SetAttributes(semconv.HTTPStatusCode(statusCode))
137
138
139 span.SetStatus(httpconv.ServerStatus(statusCode))
140 }
141
142 return reader.ReadResponse(response, consumer)
143 })
144
145 submit, err := t.transport.Submit(op)
146 if err != nil && span != nil {
147 span.RecordError(err)
148 span.SetStatus(codes.Error, err.Error())
149 }
150
151 return submit, err
152 }
153
154 func (t *openTelemetryTransport) newOpenTelemetrySpan(op *runtime.ClientOperation, header http.Header) trace.Span {
155 ctx := op.Context
156
157 tracer := t.tracer
158 if tracer == nil {
159 if span := trace.SpanFromContext(ctx); span.SpanContext().IsValid() {
160 tracer = newTracer(span.TracerProvider())
161 } else {
162 tracer = newTracer(otel.GetTracerProvider())
163 }
164 }
165
166 ctx, span := tracer.Start(ctx, t.config.SpanNameFormatter(op), t.config.SpanStartOptions...)
167
168 var scheme string
169 if len(op.Schemes) > 0 {
170 scheme = op.Schemes[0]
171 }
172
173 span.SetAttributes(
174 attribute.String("net.peer.name", t.host),
175 attribute.String(string(semconv.HTTPRouteKey), op.PathPattern),
176 attribute.String(string(semconv.HTTPMethodKey), op.Method),
177 attribute.String("span.kind", trace.SpanKindClient.String()),
178 attribute.String("http.scheme", scheme),
179 )
180
181 carrier := propagation.HeaderCarrier(header)
182 t.config.Propagator.Inject(ctx, carrier)
183
184 return span
185 }
186
187 func newTracer(tp trace.TracerProvider) trace.Tracer {
188 return tp.Tracer(tracerName, trace.WithInstrumentationVersion(version()))
189 }
190
191 func newConfig(opts ...OpenTelemetryOpt) *config {
192 c := &config{
193 Propagator: otel.GetTextMapPropagator(),
194 }
195
196 for _, opt := range opts {
197 opt.apply(c)
198 }
199
200
201 if c.TracerProvider != nil {
202 c.Tracer = newTracer(c.TracerProvider)
203 }
204
205 return c
206 }
207
208
209 func version() string {
210 return instrumentationVersion
211 }
212
View as plain text