Skip to content

Commit f5955fe

Browse files
committed
Telemetry for AWS requests
This patch add Telemetry support for all AWS requests. For context, what this patch aims is to eventually map those Telemetry spans into [semantic OpenTelemetry traces][1]. This inform what kind of metadata we need to provide on those spans: * Client: which itself includes useful information like target region. * ServiceMetadata: this is the most important one, with info about the target Service itself; * Action: the operation name, more on that bellow; * Input: the input sent by the user, more on that bellow; The `action` value is not provided on REST requests, and requires to re-generate code. A companion PR can be sent to aws-codegen project, it this feature is accepted. Given `AWS.Request` is a private implementation for the generated code, I assume this change is not breaking from user perspective. The `input` AFAICT would be useful to extract service-specific information like, for example, the table names of DynamoDB. [1]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/instrumentation/aws-sdk.md
1 parent 917cf1a commit f5955fe

File tree

6 files changed

+92
-48
lines changed

6 files changed

+92
-48
lines changed

lib/aws.ex

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,5 +44,22 @@ defmodule AWS do
4444
By default, AWS Elixir uses hackney for the HTTP client, Jason for JSON,
4545
and a custom module for XML that is written on top of xmlerl.
4646
For more details, check `AWS.Client` documentation.
47+
48+
## Telemetry
49+
50+
The following events are published:
51+
52+
* `[:aws, :request, :start]` - emitted at the beginning of the request to AWS.
53+
* Measurement: `%{system_time: System.system_time()}`
54+
* Metadata: `%{client: AWS.Client.t(), service: AWS.ServiceMetadata.t(), action: String.t(), input: map()}`
55+
56+
* `[:aws, :request, :stop]` - emitted at the end of the request to AWS.
57+
* Measurement: `%{duration: native_time}`
58+
* Metadata: `%{client: AWS.Client.t(), service: AWS.ServiceMetadata.t(), action: String.t(), input: map()}`
59+
60+
* `[:aws, :request, :exception]` - emitted when an exception has been raised.
61+
* Measurement: `%{system_time: System.system_time()}`
62+
* Metadata: `%{client: AWS.Client.t(), service: AWS.ServiceMetadata.t(), action: String.t(), input: map(),
63+
kind: Exception.kind(), reason: term(), stacktrace: Exception.stacktrace()}`
4764
"""
4865
end

lib/aws/request.ex

Lines changed: 60 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -41,22 +41,29 @@ defmodule AWS.Request do
4141
payload = encode!(client, metadata.protocol, input)
4242
headers = Signature.sign_v4(client, now(), "POST", url, headers, payload)
4343

44-
case AWS.Client.request(client, :post, url, payload, headers, options) do
45-
{:ok, %{status_code: 200, body: body} = response} ->
46-
body = if body != "", do: decode!(client, metadata.protocol, body)
47-
{:ok, body, response}
44+
telemetry_metadata = %{client: client, service: metadata, action: action, input: input}
4845

49-
{:ok, response} ->
50-
{:error, {:unexpected_response, response}}
46+
:telemetry.span([:aws, :request], telemetry_metadata, fn ->
47+
case AWS.Client.request(client, :post, url, payload, headers, options) do
48+
{:ok, %{status_code: 200, body: body} = response} ->
49+
body = if body != "", do: decode!(client, metadata.protocol, body)
50+
telemetry_metadata = Map.merge(telemetry_metadata, %{response: response, result: body})
51+
{{:ok, body, response}, telemetry_metadata}
5152

52-
error = {:error, _reason} ->
53-
error
54-
end
53+
{:ok, response} ->
54+
reason = {:unexpected_response, response}
55+
{{:error, reason}, Map.put(telemetry_metadata, :error, reason)}
56+
57+
error = {:error, reason} ->
58+
{error, Map.put(telemetry_metadata, :error, reason)}
59+
end
60+
end)
5561
end
5662

5763
def request_rest(
5864
%Client{} = client,
5965
%{} = metadata,
66+
action,
6067
http_method,
6168
path,
6269
query,
@@ -106,49 +113,55 @@ defmodule AWS.Request do
106113

107114
{response_header_parameters, options} = Keyword.pop(options, :response_header_parameters)
108115

109-
case Client.request(client, http_method, url, payload, headers, options) do
110-
{:ok, %{status_code: status_code, body: body} = response}
111-
when (is_nil(success_status_code) and status_code in 200..299) or
112-
status_code == success_status_code ->
113-
body =
114-
if body != "" do
115-
{receive_body_as_binary?, _options} = Keyword.pop(options, :receive_body_as_binary?)
116-
117-
response_body =
118-
if receive_body_as_binary? do
119-
%{"Body" => body}
120-
else
121-
decode!(client, metadata.protocol, body)
116+
telemetry_metadata = %{client: client, service: metadata, action: action, input: input}
117+
118+
:telemetry.span([:aws, :request], telemetry_metadata, fn ->
119+
case Client.request(client, http_method, url, payload, headers, options) do
120+
{:ok, %{status_code: status_code, body: body} = response}
121+
when (is_nil(success_status_code) and status_code in 200..299) or
122+
status_code == success_status_code ->
123+
body =
124+
if body != "" do
125+
{receive_body_as_binary?, _options} = Keyword.pop(options, :receive_body_as_binary?)
126+
127+
response_body =
128+
if receive_body_as_binary? do
129+
%{"Body" => body}
130+
else
131+
decode!(client, metadata.protocol, body)
132+
end
133+
134+
case response_header_parameters do
135+
[_ | _] ->
136+
response_body =
137+
if is_binary(response_body) do
138+
%{"Body" => response_body}
139+
else
140+
response_body
141+
end
142+
143+
merge_body_with_response_headers(
144+
response_body,
145+
response,
146+
response_header_parameters
147+
)
148+
149+
_ ->
150+
response_body
122151
end
123-
124-
case response_header_parameters do
125-
[_ | _] ->
126-
response_body =
127-
if is_binary(response_body) do
128-
%{"Body" => response_body}
129-
else
130-
response_body
131-
end
132-
133-
merge_body_with_response_headers(
134-
response_body,
135-
response,
136-
response_header_parameters
137-
)
138-
139-
_ ->
140-
response_body
141152
end
142-
end
143153

144-
{:ok, body, response}
154+
telemetry_metadata = Map.merge(telemetry_metadata, %{response: response, result: body})
155+
{{:ok, body, response}, telemetry_metadata}
145156

146-
{:ok, response} ->
147-
{:error, {:unexpected_response, response}}
157+
{:ok, response} ->
158+
reason = {:unexpected_response, response}
159+
{{:error, reason}, Map.put(telemetry_metadata, :error, reason)}
148160

149-
error = {:error, _reason} ->
150-
error
151-
end
161+
error = {:error, reason} ->
162+
{error, Map.put(telemetry_metadata, :error, reason)}
163+
end
164+
end)
152165
end
153166

154167
defp prepare_client(client, metadata) do

mix.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ defmodule AWS.Mixfile do
2828
[
2929
{:aws_signature, "~> 0.3"},
3030
{:jason, "~> 1.2"},
31+
{:telemetry, "~> 0.4.0 or ~> 1.0"},
3132
{:dialyxir, "~> 1.1.0", only: [:dev], runtime: false},
3233
{:earmark, "~> 1.4", only: [:dev]},
3334
{:ex_doc, "~> 0.24", only: [:dev]},

test/aws/protocol_tests/rest_json_test.exs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ defmodule AWS.ProtocolTests.RestJSONTest do
6868
Request.request_rest(
6969
client,
7070
metadata,
71+
"OperationName",
7172
:post,
7273
path,
7374
query_params,
@@ -127,6 +128,7 @@ defmodule AWS.ProtocolTests.RestJSONTest do
127128
Request.request_rest(
128129
client,
129130
metadata,
131+
"OperationName",
130132
:post,
131133
path,
132134
%{},

test/aws/protocol_tests/rest_xml_test.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ defmodule AWS.ProtocolTests.RestXMLTest do
6464
Request.request_rest(
6565
client,
6666
metadata,
67+
"OperationName",
6768
:post,
6869
path,
6970
%{},

test/aws/request_test.exs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ defmodule AWS.RequestTest do
44
alias AWS.Client
55
alias AWS.Request
66

7-
describe "request_rest/9" do
7+
describe "request_rest/10" do
88
defmodule TestClient do
99
@behaviour AWS.HTTPClient
1010

@@ -41,6 +41,7 @@ defmodule AWS.RequestTest do
4141
Request.request_rest(
4242
client,
4343
metadata,
44+
"OperationName",
4445
:get,
4546
"/foo/bar",
4647
[{"q", "x&y="}, {"size", 5}],
@@ -60,6 +61,7 @@ defmodule AWS.RequestTest do
6061
Request.request_rest(
6162
client,
6263
metadata,
64+
"OperationName",
6365
:get,
6466
"/foo/bar?format=sdk&pretty=true",
6567
[{"q", "x&y="}, {"size", 5}],
@@ -82,6 +84,7 @@ defmodule AWS.RequestTest do
8284
Request.request_rest(
8385
client,
8486
metadata,
87+
"OperationName",
8588
:post,
8689
"/foo/bar",
8790
[],
@@ -123,6 +126,7 @@ defmodule AWS.RequestTest do
123126
Request.request_rest(
124127
client,
125128
metadata,
129+
"OperationName",
126130
:post,
127131
"/foo/bar",
128132
[],
@@ -145,6 +149,7 @@ defmodule AWS.RequestTest do
145149
Request.request_rest(
146150
client,
147151
metadata,
152+
"OperationName",
148153
:post,
149154
"/foo/bar",
150155
[],
@@ -168,6 +173,7 @@ defmodule AWS.RequestTest do
168173
Request.request_rest(
169174
client,
170175
metadata,
176+
"OperationName",
171177
:post,
172178
"/foo/bar",
173179
[],
@@ -184,6 +190,7 @@ defmodule AWS.RequestTest do
184190
Request.request_rest(
185191
client,
186192
metadata,
193+
"OperationName",
187194
:post,
188195
"/foo/bar",
189196
[],
@@ -198,6 +205,7 @@ defmodule AWS.RequestTest do
198205
Request.request_rest(
199206
client,
200207
metadata,
208+
"OperationName",
201209
:post,
202210
"/foo/bar",
203211
[],
@@ -216,6 +224,7 @@ defmodule AWS.RequestTest do
216224
Request.request_rest(
217225
client,
218226
Map.put(metadata, :host_prefix, "my-prefix."),
227+
"OperationName",
219228
:get,
220229
"/foo/bar",
221230
[{"q", "x&y="}, {"size", 5}],
@@ -241,6 +250,7 @@ defmodule AWS.RequestTest do
241250
Request.request_rest(
242251
client,
243252
Map.put(metadata, :host_prefix, "{AccountId}-foo."),
253+
"OperationName",
244254
:get,
245255
"/foo/bar",
246256
[{"q", "x&y="}, {"size", 5}],

0 commit comments

Comments
 (0)