Skip to content

Commit b6aa105

Browse files
committed
impl(gax-internal): HttpSpanInfo struct, new, and update_from_outcome
1 parent 0431b38 commit b6aa105

File tree

2 files changed

+278
-0
lines changed

2 files changed

+278
-0
lines changed

src/gax-internal/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ pub mod query_parameter;
3636
#[cfg(feature = "_internal-http-client")]
3737
pub mod http;
3838

39+
#[cfg(feature = "_internal-http-client")]
40+
pub mod observability;
41+
3942
#[cfg(feature = "_internal-grpc-client")]
4043
pub mod grpc;
4144

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
use crate::options::InstrumentationClientInfo;
16+
use gax::options::RequestOptions;
17+
18+
/// Holds information extracted from HTTP requests and responses,
19+
/// formatted to align with OpenTelemetry semantic conventions for tracing.
20+
/// This struct is used to populate attributes on tracing spans.
21+
#[derive(Debug, Clone)]
22+
#[allow(dead_code)] // TODO(#3239): Remove once used in http.rs
23+
pub(crate) struct HttpSpanInfo {
24+
// Attributes for OpenTelemetry SDK interop
25+
otel_kind: String, // "Client"
26+
otel_name: String, // "{METHOD} {url.template}" or "{METHOD}"
27+
otel_status: String, // "Unset", "Ok", "Error"
28+
29+
// OpenTelemetry Semantic Conventions
30+
rpc_system: String, // "http"
31+
http_request_method: String,
32+
server_address: Option<String>, // Host from URL
33+
server_port: Option<i64>, // Port from URL
34+
url_full: String,
35+
url_scheme: Option<String>,
36+
url_template: Option<String>, // From RequestOptions.path_template
37+
url_domain: Option<String>, // Host from generator
38+
39+
http_response_status_code: Option<i64>,
40+
error_type: Option<String>,
41+
http_request_resend_count: Option<i64>,
42+
43+
// Custom GCP Attributes
44+
gcp_client_service: Option<String>,
45+
gcp_client_version: Option<String>,
46+
gcp_client_repo: String, // "googleapis/google-cloud-rust"
47+
gcp_client_artifact: Option<String>,
48+
}
49+
50+
impl HttpSpanInfo {
51+
// TODO(#3239): Remove once used in http.rs
52+
#[allow(dead_code)]
53+
pub(crate) fn from_request(
54+
request: &reqwest::Request,
55+
options: &RequestOptions,
56+
instrumentation: Option<&InstrumentationClientInfo>,
57+
prior_attempt_count: u32,
58+
) -> Self {
59+
let url = request.url();
60+
let method = request.method();
61+
62+
let url_template = gax::options::internal::get_path_template(options);
63+
let otel_name = url_template.map_or_else(
64+
|| method.to_string(),
65+
|template| format!("{} {}", method, template),
66+
);
67+
68+
let http_request_resend_count = if prior_attempt_count > 0 {
69+
Some(prior_attempt_count as i64)
70+
} else {
71+
None
72+
};
73+
74+
let (gcp_client_service, gcp_client_version, gcp_client_artifact, url_domain) =
75+
instrumentation.map_or((None, None, None, None), |info| {
76+
(
77+
Some(info.service_name.to_string()),
78+
Some(info.client_version.to_string()),
79+
Some(info.client_artifact.to_string()),
80+
Some(info.default_host.to_string()),
81+
)
82+
});
83+
84+
Self {
85+
rpc_system: "http".to_string(),
86+
otel_kind: "Client".to_string(),
87+
otel_name,
88+
otel_status: "Unset".to_string(),
89+
http_request_method: method.to_string(),
90+
server_address: url.host_str().map(String::from),
91+
server_port: url.port_or_known_default().map(|p| p as i64),
92+
url_full: url.to_string(),
93+
url_scheme: Some(url.scheme().to_string()),
94+
url_template: url_template.map(String::from),
95+
url_domain,
96+
http_response_status_code: None,
97+
error_type: None,
98+
http_request_resend_count,
99+
gcp_client_service,
100+
gcp_client_version,
101+
gcp_client_repo: "googleapis/google-cloud-rust".to_string(),
102+
gcp_client_artifact,
103+
}
104+
}
105+
106+
/// Updates the span info based on the outcome of the HTTP request.
107+
/// This method should be called after the request has completed.
108+
// TODO(#3239): Remove once used in http.rs
109+
#[allow(dead_code)]
110+
pub(crate) fn update_from_response(
111+
&mut self,
112+
result: &Result<reqwest::Response, reqwest::Error>,
113+
) {
114+
match result {
115+
Ok(response) => {
116+
self.http_response_status_code = Some(response.status().as_u16() as i64);
117+
if response.status().is_success() {
118+
self.otel_status = "Ok".to_string();
119+
} else {
120+
self.otel_status = "Error".to_string();
121+
self.error_type = Some(response.status().to_string());
122+
}
123+
}
124+
Err(err) => {
125+
self.otel_status = "Error".to_string();
126+
if err.is_timeout() {
127+
self.error_type = Some("TIMEOUT".to_string());
128+
} else if err.is_connect() {
129+
self.error_type = Some("CONNECTION_ERROR".to_string());
130+
} else if err.is_request() {
131+
self.error_type = Some("REQUEST_ERROR".to_string());
132+
} else {
133+
self.error_type = Some("UNKNOWN".to_string());
134+
}
135+
}
136+
}
137+
}
138+
}
139+
140+
#[cfg(test)]
141+
mod tests {
142+
use super::*;
143+
use crate::options::InstrumentationClientInfo;
144+
use gax::options::RequestOptions;
145+
use http::Method;
146+
use reqwest;
147+
148+
#[tokio::test]
149+
async fn test_http_span_info_from_request_basic() {
150+
let request =
151+
reqwest::Request::new(Method::GET, "https://example.com/test".parse().unwrap());
152+
let options = RequestOptions::default();
153+
154+
let span_info = HttpSpanInfo::from_request(&request, &options, None, 0);
155+
156+
assert_eq!(span_info.rpc_system, "http");
157+
assert_eq!(span_info.otel_kind, "Client");
158+
assert_eq!(span_info.otel_name, "GET");
159+
assert_eq!(span_info.otel_status, "Unset");
160+
assert_eq!(span_info.http_request_method, "GET");
161+
assert_eq!(span_info.server_address, Some("example.com".to_string()));
162+
assert_eq!(span_info.server_port, Some(443));
163+
assert_eq!(span_info.url_full, "https://example.com/test");
164+
assert_eq!(span_info.url_scheme, Some("https".to_string()));
165+
assert_eq!(span_info.url_template, None);
166+
assert_eq!(span_info.url_domain, None);
167+
assert_eq!(span_info.http_response_status_code, None);
168+
assert_eq!(span_info.error_type, None);
169+
assert_eq!(span_info.http_request_resend_count, None);
170+
assert_eq!(span_info.gcp_client_service, None);
171+
assert_eq!(span_info.gcp_client_version, None);
172+
assert_eq!(span_info.gcp_client_repo, "googleapis/google-cloud-rust");
173+
assert_eq!(span_info.gcp_client_artifact, None);
174+
}
175+
176+
#[tokio::test]
177+
async fn test_http_span_info_from_request_with_instrumentation() {
178+
let request = reqwest::Request::new(
179+
Method::POST,
180+
"https://test.service.dev:443/v1/items".parse().unwrap(),
181+
);
182+
let options = RequestOptions::default();
183+
const INFO: InstrumentationClientInfo = InstrumentationClientInfo {
184+
service_name: "test.service",
185+
client_version: "1.2.3",
186+
client_artifact: "google-cloud-test",
187+
default_host: "test.service.dev",
188+
};
189+
190+
let span_info = HttpSpanInfo::from_request(&request, &options, Some(&INFO), 0);
191+
192+
assert_eq!(
193+
span_info.gcp_client_service,
194+
Some("test.service".to_string())
195+
);
196+
assert_eq!(span_info.gcp_client_version, Some("1.2.3".to_string()));
197+
assert_eq!(
198+
span_info.gcp_client_artifact,
199+
Some("google-cloud-test".to_string())
200+
);
201+
assert_eq!(span_info.url_domain, Some("test.service.dev".to_string()));
202+
assert_eq!(
203+
span_info.server_address,
204+
Some("test.service.dev".to_string())
205+
);
206+
assert_eq!(span_info.server_port, Some(443));
207+
}
208+
209+
#[tokio::test]
210+
async fn test_http_span_info_from_request_with_path_template() {
211+
let request = reqwest::Request::new(
212+
Method::GET,
213+
"https://example.com/items/123".parse().unwrap(),
214+
);
215+
let options = gax::options::internal::set_path_template(
216+
RequestOptions::default(),
217+
Some("/items/{item_id}".to_string()),
218+
);
219+
220+
let span_info = HttpSpanInfo::from_request(&request, &options, None, 0);
221+
222+
assert_eq!(span_info.url_template, Some("/items/{item_id}".to_string()));
223+
assert_eq!(span_info.otel_name, "GET /items/{item_id}");
224+
}
225+
226+
#[tokio::test]
227+
async fn test_http_span_info_from_request_with_prior_attempt_count() {
228+
let request =
229+
reqwest::Request::new(Method::GET, "https://example.com/test".parse().unwrap());
230+
let options = RequestOptions::default();
231+
232+
// prior_attempt_count is 0 for the first try
233+
let span_info = HttpSpanInfo::from_request(&request, &options, None, 0);
234+
assert_eq!(span_info.http_request_resend_count, None);
235+
236+
// prior_attempt_count is 1 for the second try (first retry)
237+
let span_info = HttpSpanInfo::from_request(&request, &options, None, 1);
238+
assert_eq!(span_info.http_request_resend_count, Some(1));
239+
240+
let span_info = HttpSpanInfo::from_request(&request, &options, None, 5);
241+
assert_eq!(span_info.http_request_resend_count, Some(5));
242+
}
243+
244+
#[tokio::test]
245+
async fn test_update_from_response_success() {
246+
let request =
247+
reqwest::Request::new(Method::GET, "https://example.com/test".parse().unwrap());
248+
let mut span_info =
249+
HttpSpanInfo::from_request(&request, &RequestOptions::default(), None, 0);
250+
251+
let response =
252+
reqwest::Response::from(http::Response::builder().status(200).body("").unwrap());
253+
span_info.update_from_response(&Ok(response));
254+
255+
assert_eq!(span_info.otel_status, "Ok");
256+
assert_eq!(span_info.http_response_status_code, Some(200));
257+
assert_eq!(span_info.error_type, None);
258+
}
259+
260+
#[tokio::test]
261+
async fn test_update_from_response_http_error() {
262+
let request =
263+
reqwest::Request::new(Method::GET, "https://example.com/test".parse().unwrap());
264+
let mut span_info =
265+
HttpSpanInfo::from_request(&request, &RequestOptions::default(), None, 0);
266+
267+
let response =
268+
reqwest::Response::from(http::Response::builder().status(404).body("").unwrap());
269+
span_info.update_from_response(&Ok(response));
270+
271+
assert_eq!(span_info.otel_status, "Error");
272+
assert_eq!(span_info.http_response_status_code, Some(404));
273+
assert_eq!(span_info.error_type, Some("404 Not Found".to_string()));
274+
}
275+
}

0 commit comments

Comments
 (0)