To enable: ``` export RUST_LOG="warn,codex_=trace" ``` Sample: ``` Request completed method=POST url=https://chatgpt.com/backend-api/codex/responses status=200 OK headers={"date": "Wed, 14 Jan 2026 18:21:21 GMT", "transfer-encoding": "chunked", "connection": "keep-alive", "x-codex-plan-type": "business", "x-codex-primary-used-percent": "3", "x-codex-secondary-used-percent": "6", "x-codex-primary-window-minutes": "300", "x-codex-primary-over-secondary-limit-percent": "0", "x-codex-secondary-window-minutes": "10080", "x-codex-primary-reset-after-seconds": "9944", "x-codex-secondary-reset-after-seconds": "171121", "x-codex-primary-reset-at": "1768424824", "x-codex-secondary-reset-at": "1768586001", "x-codex-credits-has-credits": "False", "x-codex-credits-balance": "", "x-codex-credits-unlimited": "False", "x-models-etag": "W/\"7a7ffbc83c159dbd7a2a73aaa9c91b7a\"", "x-oai-request-id": "ffedcd30-6d8a-4c4d-be10-8ebb23c142c8", "x-envoy-upstream-service-time": "417", "x-openai-proxy-wasm": "v0.1", "cf-cache-status": "DYNAMIC", "set-cookie": "__cf_bm=xFKeaMbWNbKO5ZX.K5cJBhj34OA1QvnF_3nkdMThjlA-1768414881-1.0.1.1-uLpsE_BDkUfcmOMaeKVQmv_6_2ytnh_R3lO_il5N5K3YPQEkBo0cOMTdma6bK0Gz.hQYcIesFwKIJht1kZ9JKqAYYnjgB96hF4.sii2U3cE; path=/; expires=Wed, 14-Jan-26 18:51:21 GMT; domain=.chatgpt.com; HttpOnly; Secure; SameSite=None", "report-to": "{\"endpoints\":[{\"url\":\"https:\/\/a.nel.cloudflare.com\/report\/v4?s=4Kc7g4zUhKkIm3xHuB6ba4jyIUqqZ07ETwIPAYQASikRjA8JesbtUKDP9tSrZ5PnzWldaiSz5dZVQFI579LEsCMlMUSelTvmyQ8j4FbFDawi%2FprWZ5iRePiaSalr\"}],\"group\":\"cf-nel\",\"max_age\":604800}", "nel": "{\"success_fraction\":0.01,\"report_to\":\"cf-nel\",\"max_age\":604800}", "strict-transport-security": "max-age=31536000; includeSubDomains; preload", "x-content-type-options": "nosniff", "cross-origin-opener-policy": "same-origin-allow-popups", "referrer-policy": "strict-origin-when-cross-origin", "server": "cloudflare", "cf-ray": "9bdf270adc7aba3a-SEA"} version=HTTP/1.1 ```
218 lines
6.1 KiB
Rust
218 lines
6.1 KiB
Rust
use http::Error as HttpError;
|
|
use opentelemetry::global;
|
|
use opentelemetry::propagation::Injector;
|
|
use reqwest::IntoUrl;
|
|
use reqwest::Method;
|
|
use reqwest::Response;
|
|
use reqwest::header::HeaderMap;
|
|
use reqwest::header::HeaderName;
|
|
use reqwest::header::HeaderValue;
|
|
use serde::Serialize;
|
|
use std::fmt::Display;
|
|
use std::time::Duration;
|
|
use tracing::Span;
|
|
use tracing_opentelemetry::OpenTelemetrySpanExt;
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct CodexHttpClient {
|
|
inner: reqwest::Client,
|
|
}
|
|
|
|
impl CodexHttpClient {
|
|
pub fn new(inner: reqwest::Client) -> Self {
|
|
Self { inner }
|
|
}
|
|
|
|
pub fn get<U>(&self, url: U) -> CodexRequestBuilder
|
|
where
|
|
U: IntoUrl,
|
|
{
|
|
self.request(Method::GET, url)
|
|
}
|
|
|
|
pub fn post<U>(&self, url: U) -> CodexRequestBuilder
|
|
where
|
|
U: IntoUrl,
|
|
{
|
|
self.request(Method::POST, url)
|
|
}
|
|
|
|
pub fn request<U>(&self, method: Method, url: U) -> CodexRequestBuilder
|
|
where
|
|
U: IntoUrl,
|
|
{
|
|
let url_str = url.as_str().to_string();
|
|
CodexRequestBuilder::new(self.inner.request(method.clone(), url), method, url_str)
|
|
}
|
|
}
|
|
|
|
#[must_use = "requests are not sent unless `send` is awaited"]
|
|
#[derive(Debug)]
|
|
pub struct CodexRequestBuilder {
|
|
builder: reqwest::RequestBuilder,
|
|
method: Method,
|
|
url: String,
|
|
}
|
|
|
|
impl CodexRequestBuilder {
|
|
fn new(builder: reqwest::RequestBuilder, method: Method, url: String) -> Self {
|
|
Self {
|
|
builder,
|
|
method,
|
|
url,
|
|
}
|
|
}
|
|
|
|
fn map(self, f: impl FnOnce(reqwest::RequestBuilder) -> reqwest::RequestBuilder) -> Self {
|
|
Self {
|
|
builder: f(self.builder),
|
|
method: self.method,
|
|
url: self.url,
|
|
}
|
|
}
|
|
|
|
pub fn headers(self, headers: HeaderMap) -> Self {
|
|
self.map(|builder| builder.headers(headers))
|
|
}
|
|
|
|
pub fn header<K, V>(self, key: K, value: V) -> Self
|
|
where
|
|
HeaderName: TryFrom<K>,
|
|
<HeaderName as TryFrom<K>>::Error: Into<HttpError>,
|
|
HeaderValue: TryFrom<V>,
|
|
<HeaderValue as TryFrom<V>>::Error: Into<HttpError>,
|
|
{
|
|
self.map(|builder| builder.header(key, value))
|
|
}
|
|
|
|
pub fn bearer_auth<T>(self, token: T) -> Self
|
|
where
|
|
T: Display,
|
|
{
|
|
self.map(|builder| builder.bearer_auth(token))
|
|
}
|
|
|
|
pub fn timeout(self, timeout: Duration) -> Self {
|
|
self.map(|builder| builder.timeout(timeout))
|
|
}
|
|
|
|
pub fn json<T>(self, value: &T) -> Self
|
|
where
|
|
T: ?Sized + Serialize,
|
|
{
|
|
self.map(|builder| builder.json(value))
|
|
}
|
|
|
|
pub fn body<B>(self, body: B) -> Self
|
|
where
|
|
B: Into<reqwest::Body>,
|
|
{
|
|
self.map(|builder| builder.body(body))
|
|
}
|
|
|
|
pub async fn send(self) -> Result<Response, reqwest::Error> {
|
|
let headers = trace_headers();
|
|
|
|
match self.builder.headers(headers).send().await {
|
|
Ok(response) => {
|
|
tracing::debug!(
|
|
method = %self.method,
|
|
url = %self.url,
|
|
status = %response.status(),
|
|
headers = ?response.headers(),
|
|
version = ?response.version(),
|
|
"Request completed"
|
|
);
|
|
|
|
Ok(response)
|
|
}
|
|
Err(error) => {
|
|
let status = error.status();
|
|
tracing::debug!(
|
|
method = %self.method,
|
|
url = %self.url,
|
|
status = status.map(|s| s.as_u16()),
|
|
error = %error,
|
|
"Request failed"
|
|
);
|
|
Err(error)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct HeaderMapInjector<'a>(&'a mut HeaderMap);
|
|
|
|
impl<'a> Injector for HeaderMapInjector<'a> {
|
|
fn set(&mut self, key: &str, value: String) {
|
|
if let (Ok(name), Ok(val)) = (
|
|
HeaderName::from_bytes(key.as_bytes()),
|
|
HeaderValue::from_str(&value),
|
|
) {
|
|
self.0.insert(name, val);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn trace_headers() -> HeaderMap {
|
|
let mut headers = HeaderMap::new();
|
|
global::get_text_map_propagator(|prop| {
|
|
prop.inject_context(
|
|
&Span::current().context(),
|
|
&mut HeaderMapInjector(&mut headers),
|
|
);
|
|
});
|
|
headers
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use opentelemetry::propagation::Extractor;
|
|
use opentelemetry::propagation::TextMapPropagator;
|
|
use opentelemetry::trace::TraceContextExt;
|
|
use opentelemetry::trace::TracerProvider;
|
|
use opentelemetry_sdk::propagation::TraceContextPropagator;
|
|
use opentelemetry_sdk::trace::SdkTracerProvider;
|
|
use tracing::trace_span;
|
|
use tracing_subscriber::layer::SubscriberExt;
|
|
use tracing_subscriber::util::SubscriberInitExt;
|
|
|
|
#[test]
|
|
fn inject_trace_headers_uses_current_span_context() {
|
|
global::set_text_map_propagator(TraceContextPropagator::new());
|
|
|
|
let provider = SdkTracerProvider::builder().build();
|
|
let tracer = provider.tracer("test-tracer");
|
|
let subscriber =
|
|
tracing_subscriber::registry().with(tracing_opentelemetry::layer().with_tracer(tracer));
|
|
let _guard = subscriber.set_default();
|
|
|
|
let span = trace_span!("client_request");
|
|
let _entered = span.enter();
|
|
let span_context = span.context().span().span_context().clone();
|
|
|
|
let headers = trace_headers();
|
|
|
|
let extractor = HeaderMapExtractor(&headers);
|
|
let extracted = TraceContextPropagator::new().extract(&extractor);
|
|
let extracted_span = extracted.span();
|
|
let extracted_context = extracted_span.span_context();
|
|
|
|
assert!(extracted_context.is_valid());
|
|
assert_eq!(extracted_context.trace_id(), span_context.trace_id());
|
|
assert_eq!(extracted_context.span_id(), span_context.span_id());
|
|
}
|
|
|
|
struct HeaderMapExtractor<'a>(&'a HeaderMap);
|
|
|
|
impl<'a> Extractor for HeaderMapExtractor<'a> {
|
|
fn get(&self, key: &str) -> Option<&str> {
|
|
self.0.get(key).and_then(|value| value.to_str().ok())
|
|
}
|
|
|
|
fn keys(&self) -> Vec<&str> {
|
|
self.0.keys().map(HeaderName::as_str).collect()
|
|
}
|
|
}
|
|
}
|