Skip to content

Commit 8160972

Browse files
committed
feat(ads-client): add telemetry with tracing
1 parent 12f6cc0 commit 8160972

File tree

8 files changed

+164
-80
lines changed

8 files changed

+164
-80
lines changed

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

components/ads-client/Cargo.toml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ default = []
1212
chrono = "0.4"
1313
context_id = { path = "../context_id" }
1414
error-support = { path = "../support/error" }
15+
once_cell = "1.5"
1516
parking_lot = "0.12"
1617
rusqlite = { version = "0.37.0", features = [
1718
"functions",
@@ -21,13 +22,17 @@ rusqlite = { version = "0.37.0", features = [
2122
] }
2223
serde = "1"
2324
serde_json = "1"
25+
sql-support = { path = "../support/sql" }
2426
thiserror = "2"
25-
once_cell = "1.5"
27+
tracing = "0.1"
28+
tracing-subscriber = { version = "0.3", default-features = false, features = [
29+
"registry",
30+
"std",
31+
] }
2632
uniffi = { version = "0.29.0" }
2733
url = { version = "2", features = ["serde"] }
2834
uuid = { version = "1.3", features = ["v4"] }
2935
viaduct = { path = "../viaduct" }
30-
sql-support = { path = "../support/sql" }
3136

3237
[dev-dependencies]
3338
mockall = "0.12"

components/ads-client/src/client/ad_response.rs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ use serde::{Deserialize, Serialize};
1010
use std::collections::{HashMap, HashSet};
1111
use url::Url;
1212

13+
use tracing::error;
14+
1315
#[derive(Debug, Deserialize, PartialEq, uniffi::Record, Serialize)]
1416
pub struct AdResponse {
1517
#[serde(deserialize_with = "AdResponse::deserialize_ad_response", flatten)]
@@ -33,12 +35,7 @@ impl AdResponse {
3335
if let Ok(ad) = serde_json::from_value::<MozAd>(item) {
3436
ads.push(ad);
3537
} else {
36-
#[cfg(not(test))]
37-
{
38-
use crate::instrument::{emit_telemetry_event, TelemetryEvent};
39-
// TODO: improve the telemetry event (should we include the invalid URL?)
40-
let _ = emit_telemetry_event(Some(TelemetryEvent::InvalidUrlError));
41-
}
38+
error!(target: "ads_client::telemetry", "InvalidUrlError");
4239
}
4340
}
4441
if !ads.is_empty() {

components/ads-client/src/error.rs

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
44
*/
55

6-
use error_support::{error, ErrorHandling, GetErrorHandling};
6+
use error_support::{ErrorHandling, GetErrorHandling};
77
use viaduct::Response;
88

99
pub type AdsClientApiResult<T> = std::result::Result<T, AdsClientApiError>;
@@ -92,21 +92,6 @@ pub enum FetchAdsError {
9292
HTTPError(#[from] HTTPError),
9393
}
9494

95-
#[derive(Debug, thiserror::Error)]
96-
pub enum EmitTelemetryError {
97-
#[error("URL parse error: {0}")]
98-
UrlParse(#[from] url::ParseError),
99-
100-
#[error("Error sending request: {0}")]
101-
Request(#[from] viaduct::ViaductError),
102-
103-
#[error("JSON error: {0}")]
104-
Json(#[from] serde_json::Error),
105-
106-
#[error("Could not fetch ads, MARS responded with: {0}")]
107-
HTTPError(#[from] HTTPError),
108-
}
109-
11095
#[derive(Debug, thiserror::Error)]
11196
pub enum CallbackRequestError {
11297
#[error("Could not fetch ads, MARS responded with: {0}")]

components/ads-client/src/instrument.rs

Lines changed: 120 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3,63 +3,141 @@
33
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
44
*/
55

6-
use std::sync::LazyLock;
7-
8-
use crate::error::{ComponentError, EmitTelemetryError};
9-
use parking_lot::RwLock;
10-
use serde::{Deserialize, Serialize};
6+
use once_cell::sync::Lazy;
7+
use serde_json::json;
8+
use tracing::field::{Field, Visit};
9+
use tracing_subscriber::layer::Context;
10+
use tracing_subscriber::Layer;
1111
use url::Url;
1212
use viaduct::Request;
1313

14-
static DEFAULT_TELEMETRY_ENDPOINT: &str = "https://ads.mozilla.org/v1/log";
15-
static TELEMETRY_ENDPONT: LazyLock<RwLock<String>> =
16-
LazyLock::new(|| RwLock::new(DEFAULT_TELEMETRY_ENDPOINT.to_string()));
14+
static TELEMETRY_ENDPOINT: Lazy<Url> = Lazy::new(|| {
15+
Url::parse("https://ads.mozilla.org/v1/log")
16+
.expect("hardcoded telemetry endpoint URL must be valid")
17+
});
1718

18-
fn get_telemetry_endpoint() -> String {
19-
TELEMETRY_ENDPONT.read().clone()
19+
pub fn telemetry_layer<S>() -> impl Layer<S>
20+
where
21+
S: tracing::Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>,
22+
{
23+
TelemetryLayer {
24+
endpoint: TELEMETRY_ENDPOINT.clone(),
25+
}
26+
.with_filter(TelemetryFilter)
2027
}
2128

22-
#[derive(Debug, Deserialize, Serialize)]
23-
#[serde(rename_all = "snake_case")]
24-
pub enum TelemetryEvent {
25-
Init,
26-
RenderError,
27-
AdLoadError,
28-
FetchError,
29-
InvalidUrlError,
29+
struct TelemetryLayer {
30+
endpoint: Url,
3031
}
3132

32-
pub trait TrackError<T, ComponentError> {
33-
fn emit_telemetry_if_error(self) -> Self;
34-
}
33+
impl<S> Layer<S> for TelemetryLayer
34+
where
35+
S: tracing::Subscriber,
36+
{
37+
fn on_event(&self, event: &tracing::Event<'_>, _ctx: Context<'_, S>) {
38+
let mut visitor = EventVisitor::default();
39+
event.record(&mut visitor);
40+
41+
let event_message = visitor
42+
.fields
43+
.get("message")
44+
.unwrap_or_default()
45+
.as_str()
46+
.unwrap_or_default();
3547

36-
impl<T> TrackError<T, ComponentError> for Result<T, ComponentError> {
37-
/// Attempts to emit a telemetry event if the Error type can map to an event type.
38-
fn emit_telemetry_if_error(self) -> Self {
39-
if let Err(ref err) = self {
40-
let error_type = map_error_to_event_type(err);
41-
let _ = emit_telemetry_event(error_type);
48+
let mut url = self.endpoint.clone();
49+
url.set_query(Some(&format!("event={event_message}")));
50+
51+
if let Err(e) = Request::get(url).send() {
52+
eprintln!("[TELEMETRY] Failed to send event: {}", e);
4253
}
43-
self
4454
}
4555
}
4656

47-
fn map_error_to_event_type(err: &ComponentError) -> Option<TelemetryEvent> {
48-
match err {
49-
ComponentError::RequestAds(_) => Some(TelemetryEvent::FetchError),
50-
ComponentError::RecordImpression(_) => Some(TelemetryEvent::InvalidUrlError),
51-
ComponentError::RecordClick(_) => Some(TelemetryEvent::InvalidUrlError),
52-
ComponentError::ReportAd(_) => Some(TelemetryEvent::InvalidUrlError),
57+
struct TelemetryFilter;
58+
59+
impl<S> tracing_subscriber::layer::Filter<S> for TelemetryFilter
60+
where
61+
S: tracing::Subscriber,
62+
{
63+
fn enabled(
64+
&self,
65+
meta: &tracing::Metadata<'_>,
66+
_cx: &tracing_subscriber::layer::Context<'_, S>,
67+
) -> bool {
68+
meta.target() == "ads_client::telemetry"
69+
}
70+
}
71+
72+
#[derive(Default)]
73+
struct EventVisitor {
74+
fields: serde_json::Map<String, serde_json::Value>,
75+
}
76+
77+
impl Visit for EventVisitor {
78+
fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
79+
self.fields.insert(
80+
field.name().to_string(),
81+
serde_json::Value::String(format!("{:?}", value)),
82+
);
83+
}
84+
85+
fn record_str(&mut self, field: &Field, value: &str) {
86+
self.fields.insert(field.name().to_string(), json!(value));
87+
}
88+
89+
fn record_i64(&mut self, field: &Field, value: i64) {
90+
self.fields.insert(field.name().to_string(), json!(value));
91+
}
92+
93+
fn record_u64(&mut self, field: &Field, value: u64) {
94+
self.fields.insert(field.name().to_string(), json!(value));
95+
}
96+
97+
fn record_bool(&mut self, field: &Field, value: bool) {
98+
self.fields.insert(field.name().to_string(), json!(value));
5399
}
54100
}
55101

56-
pub fn emit_telemetry_event(event_type: Option<TelemetryEvent>) -> Result<(), EmitTelemetryError> {
57-
let endpoint = get_telemetry_endpoint();
58-
let mut url = Url::parse(&endpoint)?;
59-
if let Some(event) = event_type {
60-
let event_string = serde_json::to_string(&event)?;
61-
url.set_query(Some(&format!("event={}", event_string)));
62-
Request::get(url).send()?;
102+
#[cfg(test)]
103+
mod tests {
104+
use super::*;
105+
use mockito::mock;
106+
use tracing::error;
107+
use tracing_subscriber::prelude::*;
108+
109+
#[test]
110+
fn test_telemetry_layer() {
111+
let subscriber = tracing_subscriber::registry::Registry::default().with(telemetry_layer());
112+
tracing::subscriber::with_default(subscriber, || {});
113+
}
114+
115+
#[test]
116+
fn test_telemetry_sends_to_mock_server() {
117+
viaduct_dev::init_backend_dev();
118+
119+
let mock_server_url = mockito::server_url();
120+
let telemetry_url = Url::parse(&format!("{}/v1/log", mock_server_url)).unwrap();
121+
122+
let mock_endpoint = mock("GET", "/v1/log")
123+
.with_status(200)
124+
.match_query(mockito::Matcher::Regex(
125+
r#"event=test%20telemetry%20error"#.to_string(),
126+
))
127+
.expect(1)
128+
.create();
129+
130+
let telemetry_layer = TelemetryLayer {
131+
endpoint: telemetry_url,
132+
}
133+
.with_filter(TelemetryFilter);
134+
let subscriber = tracing_subscriber::registry::Registry::default().with(telemetry_layer);
135+
136+
tracing::subscriber::with_default(subscriber, || {
137+
error!(target: "ads_client::telemetry", message = "test telemetry error");
138+
error!(target: "ads_client::not_telemetry", message = "non-telemetry event");
139+
});
140+
141+
mock_endpoint.assert();
63142
}
64-
Ok(())
65143
}

components/ads-client/src/lib.rs

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ use error::AdsClientApiResult;
99
use error::ComponentError;
1010
use error_support::handle_error;
1111
use parking_lot::Mutex;
12+
use tracing::error;
1213
use url::Url as AdsClientUrl;
1314

1415
use client::MozAdsClientInner;
1516
use error::AdsClientApiError;
1617
use http_cache::{CacheMode, RequestCachePolicy};
17-
use instrument::TrackError;
1818

1919
use crate::client::ad_request::AdContentCategory;
2020
use crate::client::ad_request::AdPlacementRequest;
@@ -25,7 +25,7 @@ use crate::client::config::MozAdsClientConfig;
2525
mod client;
2626
mod error;
2727
pub mod http_cache;
28-
mod instrument;
28+
pub mod instrument;
2929
mod mars;
3030

3131
#[cfg(test)]
@@ -93,28 +93,37 @@ impl MozAdsClient {
9393
#[handle_error(ComponentError)]
9494
pub fn record_impression(&self, placement: MozAd) -> AdsClientApiResult<()> {
9595
let inner = self.inner.lock();
96-
inner
96+
let result = inner
9797
.record_impression(&placement)
98-
.map_err(ComponentError::RecordImpression)
99-
.emit_telemetry_if_error()
98+
.map_err(ComponentError::RecordImpression);
99+
if result.is_err() {
100+
error!(target: "ads_client::telemetry", "InvalidUrlError");
101+
}
102+
result
100103
}
101104

102105
#[handle_error(ComponentError)]
103106
pub fn record_click(&self, placement: MozAd) -> AdsClientApiResult<()> {
104107
let inner = self.inner.lock();
105-
inner
108+
let result = inner
106109
.record_click(&placement)
107-
.map_err(ComponentError::RecordClick)
108-
.emit_telemetry_if_error()
110+
.map_err(ComponentError::RecordClick);
111+
if result.is_err() {
112+
error!(target: "ads_client::telemetry", "InvalidUrlError");
113+
}
114+
result
109115
}
110116

111117
#[handle_error(ComponentError)]
112118
pub fn report_ad(&self, placement: MozAd) -> AdsClientApiResult<()> {
113119
let inner = self.inner.lock();
114-
inner
120+
let result = inner
115121
.report_ad(&placement)
116-
.map_err(ComponentError::ReportAd)
117-
.emit_telemetry_if_error()
122+
.map_err(ComponentError::ReportAd);
123+
if result.is_err() {
124+
error!(target: "ads_client::telemetry", "InvalidUrlError");
125+
}
126+
result
118127
}
119128

120129
pub fn cycle_context_id(&self) -> AdsClientApiResult<String> {

components/support/rust-log-forwarder/Cargo.toml

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,19 @@ license = "MPL-2.0"
77
exclude = ["/android", "/ios"]
88

99
[dependencies]
10+
ads-client = { path = "../../ads-client" }
1011
uniffi = { version = "0.29.0" }
11-
error-support = { path = "../error", default-features = false, features = ["tracing-logging"] }
12-
tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt", "std"] }
12+
error-support = { path = "../error", default-features = false, features = [
13+
"tracing-logging",
14+
] }
15+
tracing-subscriber = { version = "0.3", default-features = false, features = [
16+
"fmt",
17+
"std",
18+
] }
1319
tracing-support = { path = "../tracing" }
1420

1521
[dev-dependencies]
1622
tracing = "0.1"
1723

1824
[build-dependencies]
19-
uniffi = { version = "0.29.0", features=["build"]}
25+
uniffi = { version = "0.29.0", features = ["build"] }

components/support/rust-log-forwarder/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ pub fn set_logger(logger: Option<Box<dyn AppServicesLogger>>) {
6666
use tracing_subscriber::prelude::*;
6767
tracing_subscriber::registry()
6868
.with(tracing_support::simple_event_layer())
69+
.with(ads_client::instrument::telemetry_layer())
6970
.init();
7071
});
7172

0 commit comments

Comments
 (0)