@@ -8,7 +8,138 @@ use opentelemetry_sdk::{
8
8
trace:: { SpanData , SpanExporter } ,
9
9
} ;
10
10
11
+ #[ cfg( feature = "http-retry" ) ]
12
+ use crate :: retry_classification:: http:: classify_http_error;
13
+ #[ cfg( feature = "http-retry" ) ]
14
+ use opentelemetry_sdk:: retry:: { retry_with_backoff, RetryErrorType , RetryPolicy } ;
15
+ #[ cfg( feature = "http-retry" ) ]
16
+ use opentelemetry_sdk:: runtime:: Tokio ;
17
+
18
+ #[ cfg( feature = "http-retry" ) ]
19
+ /// HTTP-specific error wrapper for retry classification
20
+ #[ derive( Debug ) ]
21
+ struct HttpExportError {
22
+ status_code : u16 ,
23
+ retry_after : Option < String > ,
24
+ message : String ,
25
+ }
26
+
27
+ #[ cfg( feature = "http-retry" ) ]
28
+ /// Classify HTTP export errors for retry decisions
29
+ fn classify_http_export_error ( error : & HttpExportError ) -> RetryErrorType {
30
+ classify_http_error ( error. status_code , error. retry_after . as_deref ( ) )
31
+ }
32
+
11
33
impl SpanExporter for OtlpHttpClient {
34
+ #[ cfg( feature = "http-retry" ) ]
35
+ async fn export ( & self , batch : Vec < SpanData > ) -> OTelSdkResult {
36
+ let policy = RetryPolicy {
37
+ max_retries : 3 ,
38
+ initial_delay_ms : 100 ,
39
+ max_delay_ms : 1600 ,
40
+ jitter_ms : 100 ,
41
+ } ;
42
+
43
+ let batch = Arc :: new ( batch) ;
44
+
45
+ retry_with_backoff (
46
+ Tokio ,
47
+ policy,
48
+ classify_http_export_error,
49
+ "HttpTracesClient.Export" ,
50
+ || async {
51
+ let batch_clone = Arc :: clone ( & batch) ;
52
+
53
+ // Get client
54
+ let client = self
55
+ . client
56
+ . lock ( )
57
+ . map_err ( |e| HttpExportError {
58
+ status_code : 500 ,
59
+ retry_after : None ,
60
+ message : format ! ( "Mutex lock failed: {e}" ) ,
61
+ } ) ?
62
+ . as_ref ( )
63
+ . ok_or_else ( || HttpExportError {
64
+ status_code : 500 ,
65
+ retry_after : None ,
66
+ message : "Exporter already shutdown" . to_string ( ) ,
67
+ } ) ?
68
+ . clone ( ) ;
69
+
70
+ // Build request body
71
+ let ( body, content_type, content_encoding) = self
72
+ . build_trace_export_body ( ( * batch_clone) . clone ( ) )
73
+ . map_err ( |e| HttpExportError {
74
+ status_code : 400 ,
75
+ retry_after : None ,
76
+ message : format ! ( "Failed to build request body: {e}" ) ,
77
+ } ) ?;
78
+
79
+ // Build HTTP request
80
+ let mut request_builder = http:: Request :: builder ( )
81
+ . method ( Method :: POST )
82
+ . uri ( & self . collector_endpoint )
83
+ . header ( CONTENT_TYPE , content_type) ;
84
+
85
+ if let Some ( encoding) = content_encoding {
86
+ request_builder = request_builder. header ( "Content-Encoding" , encoding) ;
87
+ }
88
+
89
+ let mut request =
90
+ request_builder
91
+ . body ( body. into ( ) )
92
+ . map_err ( |e| HttpExportError {
93
+ status_code : 400 ,
94
+ retry_after : None ,
95
+ message : format ! ( "Failed to build HTTP request: {e}" ) ,
96
+ } ) ?;
97
+
98
+ for ( k, v) in & self . headers {
99
+ request. headers_mut ( ) . insert ( k. clone ( ) , v. clone ( ) ) ;
100
+ }
101
+
102
+ let request_uri = request. uri ( ) . to_string ( ) ;
103
+ otel_debug ! ( name: "HttpTracesClient.ExportStarted" ) ;
104
+
105
+ // Send request
106
+ let response = client. send_bytes ( request) . await . map_err ( |e| {
107
+ HttpExportError {
108
+ status_code : 0 , // Network error
109
+ retry_after : None ,
110
+ message : format ! ( "Network error: {e:?}" ) ,
111
+ }
112
+ } ) ?;
113
+
114
+ let status_code = response. status ( ) . as_u16 ( ) ;
115
+ let retry_after = response
116
+ . headers ( )
117
+ . get ( "retry-after" )
118
+ . and_then ( |v| v. to_str ( ) . ok ( ) )
119
+ . map ( |s| s. to_string ( ) ) ;
120
+
121
+ if !response. status ( ) . is_success ( ) {
122
+ return Err ( HttpExportError {
123
+ status_code,
124
+ retry_after,
125
+ message : format ! (
126
+ "HTTP export failed. Url: {}, Status: {}, Response: {:?}" ,
127
+ request_uri,
128
+ status_code,
129
+ response. body( )
130
+ ) ,
131
+ } ) ;
132
+ }
133
+
134
+ otel_debug ! ( name: "HttpTracesClient.ExportSucceeded" ) ;
135
+ Ok ( ( ) )
136
+ } ,
137
+ )
138
+ . await
139
+ . map_err ( |e| OTelSdkError :: InternalFailure ( e. message ) )
140
+ }
141
+
142
+ #[ cfg( not( feature = "http-retry" ) ) ]
12
143
async fn export ( & self , batch : Vec < SpanData > ) -> OTelSdkResult {
13
144
let client = match self
14
145
. client
0 commit comments