@@ -17,40 +17,54 @@ defmodule Tesla.Middleware.OpenTelemetry do
1717 Defaults to calling `:otel_propagator_text_map.get_text_map_injector/0`
1818 - `:mark_status_ok` - configures spans with a list of expected HTTP error codes to be marked as `ok`,
1919 not as an error-containing spans
20+ - `:opt_in_attrs` - list of opt-in and experimental attributes to be added. Use semantic conventions library to ensure compatibility, e.g. `[HTTPAttributes.http_request_body_size()]`
21+ - `:request_header_attrs` - list of request headers to be added as attributes, e.g. `["content-type"]`
22+ - `:response_header_attrs` - list of response headers to be added as attributes, e.g. `["content-length"]`
2023 """
2124
22- alias OpenTelemetry.SemanticConventions.Trace
25+ alias OpenTelemetry.SemConv.ErrorAttributes
26+ alias OpenTelemetry.SemConv.Incubating.HTTPAttributes
27+ alias OpenTelemetry.SemConv.Incubating.URLAttributes
28+ alias OpenTelemetry.SemConv.NetworkAttributes
29+ alias OpenTelemetry.SemConv.ServerAttributes
30+ alias OpenTelemetry.SemConv.UserAgentAttributes
2331
2432 require OpenTelemetry.Tracer
25- require Trace
2633
2734 @ behaviour Tesla.Middleware
2835
36+ @ otel_opts ~w[ span_name propagator mark_status_ok opt_in_attrs request_header_attrs response_header_attrs] a
37+
2938 def call ( env , next , opts ) do
30- span_name = get_span_name ( env , Keyword . get ( opts , :span_name ) )
39+ opts = opts |> Keyword . take ( @ otel_opts ) |> Enum . into ( % { } )
40+ span_name = get_span_name ( env , Map . get ( opts , :span_name ) )
3141
3242 OpenTelemetry.Tracer . with_span span_name , % { kind: :client } do
3343 env
3444 |> maybe_put_additional_ok_statuses ( opts [ :mark_status_ok ] )
35- |> maybe_propagate ( Keyword . get ( opts , :propagator , :opentelemetry . get_text_map_injector ( ) ) )
45+ |> maybe_propagate ( Map . get ( opts , :propagator , :opentelemetry . get_text_map_injector ( ) ) )
46+ |> set_req_span_attributes ( opts )
3647 |> Tesla . run ( next )
37- |> set_span_attributes ( )
48+ |> set_resp_span_attributes ( opts )
3849 |> handle_result ( )
3950 end
4051 end
4152
42- defp get_span_name ( _env , span_name ) when is_binary ( span_name ) do
43- span_name
53+ defp get_span_name ( env , span_name ) when is_binary ( span_name ) do
54+ " #{ http_method ( env . method ) } #{ span_name } "
4455 end
4556
4657 defp get_span_name ( env , span_name_fun ) when is_function ( span_name_fun , 1 ) do
47- span_name_fun . ( env )
58+ " #{ http_method ( env . method ) } #{ span_name_fun . ( env ) } "
4859 end
4960
5061 defp get_span_name ( env , _ ) do
62+ # `path_params` is used by `Tesla.Middleware.PathParams`
63+ # if `path_params` is not set, the path potentially has high cardinality and we cannot use it in the span name
64+ # if `path_params` is set, it means that the path is a template and we can use it in the span name
5165 case env . opts [ :path_params ] do
52- nil -> "HTTP #{ http_method ( env . method ) } "
53- _ -> URI . parse ( env . url ) . path
66+ nil -> "#{ http_method ( env . method ) } "
67+ _ -> " #{ http_method ( env . method ) } #{ URI . parse ( env . url ) . path } "
5468 end
5569 end
5670
@@ -73,19 +87,14 @@ defmodule Tesla.Middleware.OpenTelemetry do
7387
7488 defp maybe_put_additional_ok_statuses ( env , _additional_ok_statuses ) , do: env
7589
76- defp set_span_attributes ( { _ , % Tesla.Env { } = env } = result ) do
77- OpenTelemetry.Tracer . set_attributes ( build_attrs ( env ) )
78-
79- result
80- end
81-
82- defp set_span_attributes ( result ) do
83- result
84- end
85-
8690 defp handle_result ( { :ok , % Tesla.Env { status: status , opts: opts } = env } ) when status >= 400 do
8791 span_status =
88- if status in Keyword . get ( opts , :additional_ok_statuses , [ ] ) , do: :ok , else: :error
92+ if status in Keyword . get ( opts , :additional_ok_statuses , [ ] ) do
93+ :ok
94+ else
95+ OpenTelemetry.Tracer . set_attribute ( ErrorAttributes . error_type ( ) , "#{ status } " )
96+ :error
97+ end
8998
9099 span_status
91100 |> OpenTelemetry . status ( "" )
@@ -95,7 +104,12 @@ defmodule Tesla.Middleware.OpenTelemetry do
95104 end
96105
97106 defp handle_result ( { :error , { Tesla.Middleware.FollowRedirects , :too_many_redirects } } = result ) do
98- OpenTelemetry.Tracer . set_status ( OpenTelemetry . status ( :error , "" ) )
107+ OpenTelemetry.Tracer . set_attribute (
108+ ErrorAttributes . error_type ( ) ,
109+ Tesla.Middleware.FollowRedirects
110+ )
111+
112+ OpenTelemetry.Tracer . set_status ( OpenTelemetry . status ( :error , "Too many redirects" ) )
99113
100114 result
101115 end
@@ -104,41 +118,123 @@ defmodule Tesla.Middleware.OpenTelemetry do
104118 { :ok , env }
105119 end
106120
107- defp handle_result ( result ) do
108- OpenTelemetry.Tracer . set_status ( OpenTelemetry . status ( :error , "" ) )
121+ defp handle_result ( { :error , reason } ) do
122+ OpenTelemetry.Tracer . set_attribute ( ErrorAttributes . error_type ( ) , get_error_struct ( reason ) )
123+ OpenTelemetry.Tracer . set_status ( OpenTelemetry . status ( :error , format_error ( reason ) ) )
109124
110- result
125+ { :error , reason }
111126 end
112127
113- defp build_attrs ( % Tesla.Env {
114- method: method ,
115- url: url ,
116- status: status_code ,
117- headers: headers ,
118- query: query
119- } ) do
120- url = Tesla . build_url ( url , query )
128+ defp set_req_span_attributes ( % Tesla.Env { method: method , url: url , headers: headers } = env , opts ) do
121129 uri = URI . parse ( url )
122130
123- attrs = % {
124- Trace . http_method ( ) => http_method ( method ) ,
125- Trace . http_url ( ) => url ,
126- Trace . http_target ( ) => uri . path ,
127- Trace . net_host_name ( ) => uri . host ,
128- Trace . http_scheme ( ) => uri . scheme ,
129- Trace . http_status_code ( ) => status_code
131+ % {
132+ HTTPAttributes . http_request_method ( ) => http_method ( method ) ,
133+ ServerAttributes . server_address ( ) => uri . host ,
134+ ServerAttributes . server_port ( ) => uri . port
135+ }
136+ |> add_opt_in_req_attrs ( env , opts )
137+ |> add_req_headers ( headers , opts )
138+ |> OpenTelemetry.Tracer . set_attributes ( )
139+
140+ env
141+ end
142+
143+ defp set_resp_span_attributes (
144+ { :ok , % Tesla.Env { url: url , query: query , status: status_code , headers: headers } = env } ,
145+ opts
146+ ) do
147+ url = Tesla . build_url ( url , query )
148+
149+ % {
150+ # Sets url.full only after the request is completed so that all middleware has been called
151+ URLAttributes . url_full ( ) => url ,
152+ HTTPAttributes . http_response_status_code ( ) => status_code
153+ }
154+ |> add_opt_in_resp_attrs ( env , opts )
155+ |> add_resp_headers ( headers , opts )
156+ |> OpenTelemetry.Tracer . set_attributes ( )
157+
158+ { :ok , env }
159+ end
160+
161+ defp set_resp_span_attributes ( { :error , _ } = result , _opts ) do
162+ result
163+ end
164+
165+ defp add_opt_in_req_attrs ( attrs , env , % { opt_in_attrs: [ _ | _ ] = opt_in_attrs } ) do
166+ uri = URI . parse ( env . url )
167+
168+ % {
169+ HTTPAttributes . http_request_body_size ( ) => get_header ( env . headers , "content-length" , "0" ) ,
170+ NetworkAttributes . network_transport ( ) => :tcp ,
171+ URLAttributes . url_scheme ( ) => uri . scheme ,
172+ URLAttributes . url_template ( ) => get_url_template ( env ) ,
173+ UserAgentAttributes . user_agent_original ( ) => get_header ( env . headers , "user-agent" , "" )
130174 }
175+ |> Map . take ( opt_in_attrs )
176+ |> then ( & Map . merge ( attrs , & 1 ) )
177+ end
178+
179+ defp add_opt_in_req_attrs ( attrs , _uri , _opts ) , do: attrs
180+
181+ defp add_opt_in_resp_attrs ( attrs , env , % { opt_in_attrs: [ _ | _ ] = opt_in_attrs } ) do
182+ % {
183+ HTTPAttributes . http_response_body_size ( ) => get_header ( env . headers , "content-length" , "0" )
184+ }
185+ |> Map . take ( opt_in_attrs )
186+ |> then ( & Map . merge ( attrs , & 1 ) )
187+ end
131188
132- maybe_append_content_length ( attrs , headers )
189+ defp add_opt_in_resp_attrs ( attrs , _env , _opts ) , do: attrs
190+
191+ defp add_req_headers (
192+ attrs ,
193+ req_headers ,
194+ % { request_header_attrs: [ _ | _ ] = request_header_attrs }
195+ ) do
196+ Map . merge (
197+ attrs ,
198+ :otel_http . extract_headers_attributes (
199+ :request ,
200+ req_headers ,
201+ request_header_attrs
202+ )
203+ )
204+ end
205+
206+ defp add_req_headers ( attrs , _req_headers , _opts ) , do: attrs
207+
208+ defp add_resp_headers (
209+ attrs ,
210+ resp_headers ,
211+ % { response_header_attrs: [ _ | _ ] = response_header_attrs }
212+ ) do
213+ Map . merge (
214+ attrs ,
215+ :otel_http . extract_headers_attributes (
216+ :response ,
217+ resp_headers ,
218+ response_header_attrs
219+ )
220+ )
133221 end
134222
135- defp maybe_append_content_length ( attrs , headers ) do
136- case Enum . find ( headers , fn { k , _v } -> k == "content-length" end ) do
137- nil ->
138- attrs
223+ defp add_resp_headers ( attrs , _resp_headers , _opts ) , do: attrs
139224
140- { _key , content_length } ->
141- Map . put ( attrs , Trace . http_response_content_length ( ) , content_length )
225+ defp get_url_template ( env ) do
226+ case env . opts [ :path_params ] do
227+ nil -> ""
228+ _ -> URI . parse ( env . url ) . path
229+ end
230+ end
231+
232+ defp get_header ( headers , header_name , default ) do
233+ headers
234+ |> Enum . find ( fn { k , _v } -> k == header_name end )
235+ |> case do
236+ nil -> default
237+ { _key , value } -> value
142238 end
143239 end
144240
@@ -147,4 +243,10 @@ defmodule Tesla.Middleware.OpenTelemetry do
147243 |> Atom . to_string ( )
148244 |> String . upcase ( )
149245 end
246+
247+ defp format_error ( % { __exception__: true } = exception ) , do: Exception . message ( exception )
248+ defp format_error ( reason ) , do: inspect ( reason )
249+
250+ defp get_error_struct ( % { __struct__: struct } ) , do: struct
251+ defp get_error_struct ( _ ) , do: ""
150252end
0 commit comments