Skip to content

Commit 020965d

Browse files
authored
Merge pull request #45 from frigus02/e2e-test
Automated test for http request content
2 parents e04a503 + 07755fe commit 020965d

File tree

6 files changed

+468
-11
lines changed

6 files changed

+468
-11
lines changed

Cargo.toml

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,17 +46,19 @@ thiserror = "1"
4646
ureq = { version = "2", optional = true }
4747

4848
[dev-dependencies]
49-
async-std = { version = "1.9.0", features = ["attributes"] }
50-
backtrace = "0.3.60"
49+
async-std = { version = "1.10.0", features = ["attributes"] }
50+
backtrace = "0.3.64"
5151
doc-comment = "0.3.3"
5252
env_logger = "0.9.0"
53-
opentelemetry = { version = "0.17", features = ["rt-tokio"] }
53+
insta = "1.12.0"
54+
opentelemetry = { version = "0.17", features = ["rt-async-std", "rt-tokio", "rt-tokio-current-thread"] }
5455
opentelemetry-application-insights = { path = ".", features = ["reqwest-client", "reqwest-blocking-client"] }
5556
rand = "0.8.4"
56-
surf = "2.2.0"
57-
test-case = "1.1.0"
58-
tokio = { version = "1.7.0", features = ["rt", "macros", "process", "time"] }
59-
version-sync = { version = "0.9.2", default-features = false, features = ["html_root_url_updated", "contains_regex"] }
57+
regex = "1.5.4"
58+
surf = "2.3.2"
59+
test-case = "1.2.1"
60+
tokio = { version = "1.16.1", features = ["rt", "macros", "process", "time"] }
61+
version-sync = { version = "0.9.4", default-features = false, features = ["html_root_url_updated", "contains_regex"] }
6062

6163
[badges]
6264
github = { repository = "frigus02/opentelemetry-application-insights", workflow = "CI" }

examples/attributes.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,7 @@ use std::env;
88

99
fn log() {
1010
get_active_span(|span| {
11-
span.add_event(
12-
"An event!".to_string(),
13-
vec![KeyValue::new("happened", true)],
14-
);
11+
span.add_event("An event!", vec![KeyValue::new("happened", true)]);
1512
})
1613
}
1714

tests/http_requests.rs

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
//! Snapshot tests for generated HTTP requests
2+
//!
3+
//! # Update snapshots
4+
//!
5+
//! ```
6+
//! INSTA_UPDATE=always cargo test
7+
//! ```
8+
9+
use format::requests_to_string;
10+
use opentelemetry::{
11+
sdk::{trace::Config, Resource},
12+
trace::{get_active_span, SpanKind, Tracer, TracerProvider},
13+
KeyValue,
14+
};
15+
use opentelemetry_application_insights::new_pipeline;
16+
use opentelemetry_semantic_conventions as semcov;
17+
use recording_client::record;
18+
use std::time::Duration;
19+
use tick::{AsyncStdTick, NoTick, TokioTick};
20+
21+
// Fake instrumentation key (this is a random uuid)
22+
const INSTRUMENTATION_KEY: &str = "0fdcec70-0ce5-4085-89d9-9ae8ead9af66";
23+
24+
#[test]
25+
fn traces_simple() {
26+
let requests = record(NoTick, |client| {
27+
// Fake instrumentation key (this is a random uuid)
28+
let client_provider = new_pipeline(INSTRUMENTATION_KEY.into())
29+
.with_client(client.clone())
30+
.with_trace_config(Config::default().with_resource(Resource::new(vec![
31+
semcov::resource::SERVICE_NAMESPACE.string("test"),
32+
semcov::resource::SERVICE_NAME.string("client"),
33+
])))
34+
.build_simple();
35+
let client_tracer = client_provider.tracer("test");
36+
37+
let server_provider = new_pipeline(INSTRUMENTATION_KEY.into())
38+
.with_client(client)
39+
.with_trace_config(Config::default().with_resource(Resource::new(vec![
40+
semcov::resource::SERVICE_NAMESPACE.string("test"),
41+
semcov::resource::SERVICE_NAME.string("server"),
42+
])))
43+
.build_simple();
44+
let server_tracer = server_provider.tracer("test");
45+
46+
// An HTTP client make a request
47+
let span = client_tracer
48+
.span_builder("dependency")
49+
.with_kind(SpanKind::Client)
50+
.with_attributes(vec![
51+
semcov::trace::ENDUSER_ID.string("marry"),
52+
semcov::trace::NET_HOST_NAME.string("localhost"),
53+
semcov::trace::NET_PEER_IP.string("10.1.2.4"),
54+
semcov::trace::HTTP_URL.string("http://10.1.2.4/hello/world?name=marry"),
55+
semcov::trace::HTTP_STATUS_CODE.string("200"),
56+
])
57+
.start(&client_tracer);
58+
client_tracer.with_span(span, |cx| {
59+
// The server receives the request
60+
let builder = server_tracer
61+
.span_builder("request")
62+
.with_kind(SpanKind::Server)
63+
.with_attributes(vec![
64+
semcov::trace::ENDUSER_ID.string("marry"),
65+
semcov::trace::NET_HOST_NAME.string("localhost"),
66+
semcov::trace::NET_PEER_IP.string("10.1.2.3"),
67+
semcov::trace::HTTP_TARGET.string("/hello/world?name=marry"),
68+
semcov::trace::HTTP_STATUS_CODE.string("200"),
69+
]);
70+
let span = server_tracer.build_with_context(builder, &cx);
71+
server_tracer.with_span(span, |_cx| {
72+
get_active_span(|span| {
73+
span.add_event("An event!", vec![KeyValue::new("happened", true)]);
74+
let error: Box<dyn std::error::Error> = "An error".into();
75+
span.record_exception_with_stacktrace(error.as_ref(), "a backtrace");
76+
});
77+
});
78+
79+
// Force the server span to be sent before the client span. Without this on Jan's PC
80+
// the server span gets sent after the client span, but on GitHub Actions it's the
81+
// other way around.
82+
std::thread::sleep(Duration::from_secs(1));
83+
});
84+
});
85+
let traces_simple = requests_to_string(requests);
86+
insta::assert_snapshot!(traces_simple);
87+
}
88+
89+
#[async_std::test]
90+
async fn traces_batch_async_std() {
91+
let requests = record(AsyncStdTick, |client| {
92+
let tracer_provider = new_pipeline(INSTRUMENTATION_KEY.into())
93+
.with_client(client)
94+
.build_batch(opentelemetry::runtime::AsyncStd);
95+
let tracer = tracer_provider.tracer("test");
96+
97+
tracer.in_span("async-std", |_cx| {});
98+
});
99+
let traces_batch_async_std = requests_to_string(requests);
100+
insta::assert_snapshot!(traces_batch_async_std);
101+
}
102+
103+
#[tokio::test]
104+
async fn traces_batch_tokio() {
105+
let requests = record(TokioTick, |client| {
106+
let tracer_provider = new_pipeline(INSTRUMENTATION_KEY.into())
107+
.with_client(client)
108+
.build_batch(opentelemetry::runtime::TokioCurrentThread);
109+
let tracer = tracer_provider.tracer("test");
110+
111+
tracer.in_span("tokio", |_cx| {});
112+
});
113+
let traces_batch_tokio = requests_to_string(requests);
114+
insta::assert_snapshot!(traces_batch_tokio);
115+
}
116+
117+
mod recording_client {
118+
use super::tick::Tick;
119+
use async_trait::async_trait;
120+
use bytes::Bytes;
121+
use http::{Request, Response};
122+
use opentelemetry_http::{HttpClient, HttpError};
123+
use std::{
124+
sync::{Arc, Mutex},
125+
time::Duration,
126+
};
127+
128+
#[derive(Debug, Clone)]
129+
pub struct RecordingClient {
130+
requests: Arc<Mutex<Vec<Request<Vec<u8>>>>>,
131+
tick: Arc<dyn Tick>,
132+
}
133+
134+
#[async_trait]
135+
impl HttpClient for RecordingClient {
136+
async fn send(&self, req: Request<Vec<u8>>) -> Result<Response<Bytes>, HttpError> {
137+
self.tick.tick().await;
138+
self.requests
139+
.lock()
140+
.expect("requests mutex is healthy")
141+
.push(req);
142+
Ok(Response::builder()
143+
.status(200)
144+
.body(Bytes::from("{}"))
145+
.expect("response is fell formed"))
146+
}
147+
}
148+
149+
pub fn record(
150+
tick: impl Tick + 'static,
151+
generate_fn: impl Fn(RecordingClient),
152+
) -> Vec<Request<Vec<u8>>> {
153+
let requests = Arc::new(Mutex::new(Vec::new()));
154+
generate_fn(RecordingClient {
155+
requests: Arc::clone(&requests),
156+
tick: Arc::new(tick),
157+
});
158+
159+
// Give async runtime some time to quit. I don't see any way to properly wait for tasks
160+
// spawned with async-std.
161+
std::thread::sleep(Duration::from_secs(1));
162+
163+
Arc::try_unwrap(requests)
164+
.expect("client is dropped everywhere")
165+
.into_inner()
166+
.expect("requests mutex is healthy")
167+
}
168+
}
169+
170+
mod tick {
171+
use async_trait::async_trait;
172+
use std::{fmt::Debug, time::Duration};
173+
174+
#[async_trait]
175+
pub trait Tick: Debug + Send + Sync {
176+
async fn tick(&self);
177+
}
178+
179+
#[derive(Debug)]
180+
pub struct NoTick;
181+
182+
#[async_trait]
183+
impl Tick for NoTick {
184+
async fn tick(&self) {}
185+
}
186+
187+
#[derive(Debug)]
188+
pub struct AsyncStdTick;
189+
190+
#[async_trait]
191+
impl Tick for AsyncStdTick {
192+
async fn tick(&self) {
193+
async_std::task::sleep(Duration::from_millis(1)).await;
194+
}
195+
}
196+
197+
#[derive(Debug)]
198+
pub struct TokioTick;
199+
200+
#[async_trait]
201+
impl Tick for TokioTick {
202+
async fn tick(&self) {
203+
tokio::time::sleep(Duration::from_millis(1)).await;
204+
}
205+
}
206+
}
207+
208+
mod format {
209+
use http::Request;
210+
use regex::Regex;
211+
212+
pub fn requests_to_string(requests: Vec<Request<Vec<u8>>>) -> String {
213+
requests
214+
.into_iter()
215+
.map(request_to_string)
216+
.collect::<Vec<_>>()
217+
.join("\n\n\n")
218+
}
219+
220+
fn request_to_string(req: Request<Vec<u8>>) -> String {
221+
let method = req.method();
222+
let path = req.uri().path_and_query().expect("path exists");
223+
let version = format!("{:?}", req.version());
224+
let host = req.uri().authority().expect("authority exists");
225+
let headers = req
226+
.headers()
227+
.into_iter()
228+
.map(|(name, value)| {
229+
let value = value.to_str().expect("header value is valid string");
230+
format!("{name}: {value}")
231+
})
232+
.collect::<Vec<_>>()
233+
.join("\n");
234+
let body = strip_changing_values(&pretty_print_json(req.body()));
235+
format!("{method} {path} {version}\nhost: {host}\n{headers}\n\n{body}")
236+
}
237+
238+
fn strip_changing_values(body: &str) -> String {
239+
let res = vec![
240+
Regex::new(r#""(?P<field>time)": "\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z""#)
241+
.unwrap(),
242+
Regex::new(r#""(?P<field>duration)": "\d+\.\d{2}:\d{2}:\d{2}\.\d{6}""#).unwrap(),
243+
Regex::new(r#""(?P<field>id|ai\.operation\.parentId)": "[a-z0-9]{16}""#).unwrap(),
244+
Regex::new(r#""(?P<field>ai\.operation\.id)": "[a-z0-9]{32}""#).unwrap(),
245+
];
246+
247+
res.into_iter().fold(body.into(), |body, re| {
248+
re.replace_all(&body, r#""$field": "STRIPPED""#).into()
249+
})
250+
}
251+
252+
fn pretty_print_json(body: &[u8]) -> String {
253+
let json: serde_json::Value = serde_json::from_slice(body).expect("body is valid json");
254+
serde_json::to_string_pretty(&json).unwrap()
255+
}
256+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
source: tests/http_requests.rs
3+
assertion_line: 131
4+
expression: traces_batch_async_std
5+
6+
---
7+
POST /v2/track HTTP/1.1
8+
host: dc.services.visualstudio.com
9+
content-type: application/json
10+
11+
[
12+
{
13+
"data": {
14+
"baseData": {
15+
"duration": "STRIPPED",
16+
"id": "STRIPPED",
17+
"name": "async-std",
18+
"properties": {
19+
"service.name": "unknown_service"
20+
},
21+
"resultCode": "0",
22+
"type": "InProc",
23+
"ver": 2
24+
},
25+
"baseType": "RemoteDependencyData"
26+
},
27+
"iKey": "0fdcec70-0ce5-4085-89d9-9ae8ead9af66",
28+
"name": "Microsoft.ApplicationInsights.RemoteDependency",
29+
"sampleRate": 100.0,
30+
"tags": {
31+
"ai.cloud.role": "unknown_service",
32+
"ai.operation.id": "STRIPPED"
33+
},
34+
"time": "STRIPPED"
35+
}
36+
]
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
source: tests/http_requests.rs
3+
assertion_line: 145
4+
expression: traces_batch_tokio
5+
6+
---
7+
POST /v2/track HTTP/1.1
8+
host: dc.services.visualstudio.com
9+
content-type: application/json
10+
11+
[
12+
{
13+
"data": {
14+
"baseData": {
15+
"duration": "STRIPPED",
16+
"id": "STRIPPED",
17+
"name": "tokio",
18+
"properties": {
19+
"service.name": "unknown_service"
20+
},
21+
"resultCode": "0",
22+
"type": "InProc",
23+
"ver": 2
24+
},
25+
"baseType": "RemoteDependencyData"
26+
},
27+
"iKey": "0fdcec70-0ce5-4085-89d9-9ae8ead9af66",
28+
"name": "Microsoft.ApplicationInsights.RemoteDependency",
29+
"sampleRate": 100.0,
30+
"tags": {
31+
"ai.cloud.role": "unknown_service",
32+
"ai.operation.id": "STRIPPED"
33+
},
34+
"time": "STRIPPED"
35+
}
36+
]

0 commit comments

Comments
 (0)