22// SPDX-License-Identifier: Apache-2.0
33
44use std:: collections:: HashMap ;
5- use std:: sync:: { Arc , Mutex } ;
5+ use std:: sync:: { Arc , Mutex , OnceLock } ;
66
77use aws_credential_types:: provider:: SharedCredentialsProvider ;
88use aws_credential_types:: Credentials ;
@@ -17,6 +17,8 @@ use opentelemetry_sdk::trace::{
1717use wiremock:: matchers:: any;
1818use wiremock:: { Mock , MockServer as WireMockServer , Request , Respond , ResponseTemplate } ;
1919
20+ const HTTP_URL : & str = "http.url" ;
21+
2022#[ derive( Clone ) ]
2123struct CaptureBodyResponder {
2224 status : u16 ,
@@ -56,27 +58,28 @@ impl MockAwsServer {
5658/// 2. Build the AWS client with `harness.sdk_config()`.
5759/// 3. Execute SDK calls, then assert on `harness.finished_spans()` and `harness.server.bodies()`.
5860///
59- /// Tests should remain serialized (`#[serial]`) because harness creation sets global
60- /// OpenTelemetry tracer/propagator state via `init_test_tracer`.
61+ /// The global OpenTelemetry tracer and propagator are initialized once per test
62+ /// binary. Each harness filters exported spans to requests sent to its mock
63+ /// server, so tests can run concurrently.
6164pub struct TestHarness {
62- pub exporter : InMemorySpanExporter ,
65+ exporter : InMemorySpanExporter ,
6366 pub server : MockAwsServer ,
6467}
6568
6669impl TestHarness {
67- /// Initializes tracing and starts a mock AWS endpoint returning `200 OK`
70+ /// Ensures tracing is initialized and starts a mock AWS endpoint returning `200 OK`
6871 /// for every request.
6972 pub async fn ok ( ) -> Self {
7073 Self :: from_status ( 200 ) . await
7174 }
7275
73- /// Initializes tracing and starts a mock AWS endpoint returning `200 OK`
76+ /// Ensures tracing is initialized and starts a mock AWS endpoint returning `200 OK`
7477 /// with the supplied response body.
7578 pub async fn ok_with_body ( response_body : impl Into < String > ) -> Self {
7679 Self :: from_status_and_body ( 200 , response_body) . await
7780 }
7881
79- /// Initializes tracing and starts a mock AWS endpoint returning
82+ /// Ensures tracing is initialized and starts a mock AWS endpoint returning
8083 /// `400 Bad Request` for every request.
8184 pub async fn bad_request ( ) -> Self {
8285 Self :: from_status ( 400 ) . await
@@ -89,7 +92,12 @@ impl TestHarness {
8992
9093 /// Returns all finished spans currently exported by this harness.
9194 pub fn finished_spans ( & self ) -> Vec < SpanData > {
92- self . exporter . get_finished_spans ( ) . unwrap ( )
95+ self . exporter
96+ . get_finished_spans ( )
97+ . unwrap ( )
98+ . into_iter ( )
99+ . filter ( |span| span_targets_server ( span, & self . server . url ) )
100+ . collect ( )
93101 }
94102
95103 async fn from_status ( status : u16 ) -> Self {
@@ -135,15 +143,35 @@ pub async fn mock_aws_with_body(status: u16, response_body: impl Into<String>) -
135143}
136144
137145/// Registers an `InMemorySpanExporter` as the global tracer provider and
138- /// a `TraceContextPropagator` as the global propagator.
146+ /// a `TraceContextPropagator` as the global propagator once per test binary .
139147pub fn init_test_tracer ( ) -> InMemorySpanExporter {
140- let exporter = InMemorySpanExporter :: default ( ) ;
141- let provider = SdkTracerProvider :: builder ( )
142- . with_span_processor ( SimpleSpanProcessor :: new ( exporter. clone ( ) ) )
143- . build ( ) ;
144- global:: set_tracer_provider ( provider) ;
145- global:: set_text_map_propagator ( TraceContextPropagator :: new ( ) ) ;
146- exporter
148+ static EXPORTER : OnceLock < InMemorySpanExporter > = OnceLock :: new ( ) ;
149+
150+ EXPORTER
151+ . get_or_init ( || {
152+ let exporter = InMemorySpanExporter :: default ( ) ;
153+ let provider = SdkTracerProvider :: builder ( )
154+ . with_span_processor ( SimpleSpanProcessor :: new ( exporter. clone ( ) ) )
155+ . build ( ) ;
156+ global:: set_tracer_provider ( provider) ;
157+ global:: set_text_map_propagator ( TraceContextPropagator :: new ( ) ) ;
158+ exporter
159+ } )
160+ . clone ( )
161+ }
162+
163+ fn span_targets_server ( span : & SpanData , server_url : & str ) -> bool {
164+ span. attributes . iter ( ) . any ( |kv| {
165+ kv. key . as_str ( ) == HTTP_URL && url_targets_server ( & kv. value . to_string ( ) , server_url)
166+ } )
167+ }
168+
169+ fn url_targets_server ( url : & str , server_url : & str ) -> bool {
170+ let Some ( rest) = url. strip_prefix ( server_url) else {
171+ return false ;
172+ } ;
173+
174+ rest. is_empty ( ) || rest. starts_with ( '/' ) || rest. starts_with ( '?' )
147175}
148176
149177/// Builds an `SdkConfig` pointing at `endpoint` with test credentials.
@@ -179,3 +207,22 @@ pub fn split_traceparent(tp: &str) -> (String, String) {
179207 let parts: Vec < & str > = tp. splitn ( 4 , '-' ) . collect ( ) ;
180208 ( parts[ 1 ] . to_string ( ) , parts[ 2 ] . to_string ( ) )
181209}
210+
211+ #[ cfg( test) ]
212+ mod tests {
213+ use super :: * ;
214+
215+ #[ test]
216+ fn url_targets_server_requires_url_boundary ( ) {
217+ let server_url = "http://127.0.0.1:1234" ;
218+
219+ assert ! ( url_targets_server( server_url, server_url) ) ;
220+ assert ! ( url_targets_server( "http://127.0.0.1:1234/" , server_url) ) ;
221+ assert ! ( url_targets_server(
222+ "http://127.0.0.1:1234/?Action=PutTargets" ,
223+ server_url
224+ ) ) ;
225+ assert ! ( !url_targets_server( "http://127.0.0.1:12345/" , server_url) ) ;
226+ assert ! ( !url_targets_server( "http://127.0.0.1:123" , server_url) ) ;
227+ }
228+ }
0 commit comments