Skip to content

Commit 0a02aba

Browse files
Conform attributes and span name to v1.27 semantics for OpentelemetryTesla (#469)
* Conform attributes and span name to v1.27 semantics for OpentelemetryTesla * list out the aliases as fully qualified; use the semantic conventions for matches in tests * Add comment for path_params in get_span_name/2 function --------- Co-authored-by: Tristan Sloughter <[email protected]>
1 parent 0fcfbed commit 0a02aba

File tree

4 files changed

+317
-71
lines changed

4 files changed

+317
-71
lines changed

instrumentation/opentelemetry_tesla/lib/middleware/opentelemetry_tesla_middleware.ex

Lines changed: 149 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -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: ""
150252
end

instrumentation/opentelemetry_tesla/mix.exs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,9 @@ defmodule OpentelemetryTesla.MixProject do
5757
{:opentelemetry, "~> 1.0", only: :test},
5858
{:opentelemetry_api, "~> 1.2"},
5959
{:opentelemetry_telemetry, "~> 1.1"},
60-
{:opentelemetry_semantic_conventions, "~> 0.2"},
60+
{:opentelemetry_semantic_conventions, "~> 1.27"},
6161
{:tesla, "~> 1.4"},
62+
{:otel_http, "~> 0.2"},
6263
{:ex_doc, "~> 0.38", only: :dev, runtime: false},
6364
{:bypass, "~> 2.1", only: :test},
6465
{:jason, "~> 1.3", only: :test}

instrumentation/opentelemetry_tesla/mix.lock

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@
1313
"nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"},
1414
"opentelemetry": {:hex, :opentelemetry, "1.5.0", "7dda6551edfc3050ea4b0b40c0d2570423d6372b97e9c60793263ef62c53c3c2", [:rebar3], [{:opentelemetry_api, "~> 1.4", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}], "hexpm", "cdf4f51d17b592fc592b9a75f86a6f808c23044ba7cf7b9534debbcc5c23b0ee"},
1515
"opentelemetry_api": {:hex, :opentelemetry_api, "1.4.0", "63ca1742f92f00059298f478048dfb826f4b20d49534493d6919a0db39b6db04", [:mix, :rebar3], [], "hexpm", "3dfbbfaa2c2ed3121c5c483162836c4f9027def469c41578af5ef32589fcfc58"},
16-
"opentelemetry_semantic_conventions": {:hex, :opentelemetry_semantic_conventions, "0.2.0", "b67fe459c2938fcab341cb0951c44860c62347c005ace1b50f8402576f241435", [:mix, :rebar3], [], "hexpm", "d61fa1f5639ee8668d74b527e6806e0503efc55a42db7b5f39939d84c07d6895"},
16+
"opentelemetry_semantic_conventions": {:hex, :opentelemetry_semantic_conventions, "1.27.0", "acd0194a94a1e57d63da982ee9f4a9f88834ae0b31b0bd850815fe9be4bbb45f", [:mix, :rebar3], [], "hexpm", "9681ccaa24fd3d810b4461581717661fd85ff7019b082c2dff89c7d5b1fc2864"},
1717
"opentelemetry_telemetry": {:hex, :opentelemetry_telemetry, "1.1.2", "410ab4d76b0921f42dbccbe5a7c831b8125282850be649ee1f70050d3961118a", [:mix, :rebar3], [{:opentelemetry_api, "~> 1.3", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "641ab469deb181957ac6d59bce6e1321d5fe2a56df444fc9c19afcad623ab253"},
18+
"otel_http": {:hex, :otel_http, "0.2.0", "b17385986c7f1b862f5d577f72614ecaa29de40392b7618869999326b9a61d8a", [:rebar3], [], "hexpm", "f2beadf922c8cfeb0965488dd736c95cc6ea8b9efce89466b3904d317d7cc717"},
1819
"plug": {:hex, :plug, "1.15.3", "712976f504418f6dff0a3e554c40d705a9bcf89a7ccef92fc6a5ef8f16a30a97", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cc4365a3c010a56af402e0809208873d113e9c38c401cabd88027ef4f5c01fd2"},
1920
"plug_cowboy": {:hex, :plug_cowboy, "2.7.0", "3ae9369c60641084363b08fe90267cbdd316df57e3557ea522114b30b63256ea", [:mix], [{:cowboy, "~> 2.7.0 or ~> 2.8.0 or ~> 2.9.0 or ~> 2.10.0", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d85444fb8aa1f2fc62eabe83bbe387d81510d773886774ebdcb429b3da3c1a4a"},
2021
"plug_crypto": {:hex, :plug_crypto, "2.0.0", "77515cc10af06645abbfb5e6ad7a3e9714f805ae118fa1a70205f80d2d70fe73", [:mix], [], "hexpm", "53695bae57cc4e54566d993eb01074e4d894b65a3766f1c43e2c61a1b0f45ea9"},

0 commit comments

Comments
 (0)