Skip to content

Commit

Permalink
fix: track usage metrics (#1875)
Browse files Browse the repository at this point in the history
Co-authored-by: Tushar Mathur <[email protected]>
  • Loading branch information
amitksingh1490 and tusharmath authored May 7, 2024
1 parent e77b43e commit 756a56f
Show file tree
Hide file tree
Showing 9 changed files with 499 additions and 9 deletions.
280 changes: 276 additions & 4 deletions Cargo.lock

Large diffs are not rendered by default.

9 changes: 5 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -142,14 +142,14 @@ dotenvy = "0.15.7"
convert_case = "0.6.0"
rand = "0.8.5"
tailcall-macros = { path = "tailcall-macros" }
tailcall-tracker = { path = "tailcall-tracker", optional = true }
tonic-types = "0.11.0"
datatest-stable = "0.2.9"
tokio-test = "0.4.4"
base64 = "0.22.1"


[dev-dependencies]
tailcall-prettier = {path = "tailcall-prettier"}
tailcall-prettier = { path = "tailcall-prettier" }
criterion = "0.5.1"
httpmock = "0.7.0"
pretty_assertions = "1.4.0"
Expand Down Expand Up @@ -185,6 +185,7 @@ cli = [
"opentelemetry_sdk/rt-tokio",
"dep:opentelemetry-otlp",
"dep:opentelemetry-system-metrics",
"dep:tailcall-tracker"
]

# Feature flag to enable all default features.
Expand All @@ -203,8 +204,8 @@ members = [
"tailcall-prettier",
"tailcall-query-plan",
"tailcall-fixtures",
"tailcall-upstream-grpc"
]
"tailcall-upstream-grpc",
"tailcall-tracker"]

# Boost execution_spec snapshot diffing performance
[profile.dev.package]
Expand Down
3 changes: 2 additions & 1 deletion src/cli/command.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use clap::{Parser, Subcommand};
use strum_macros::Display;

use crate::{config, generator};

Expand All @@ -20,7 +21,7 @@ pub struct Cli {
pub command: Command,
}

#[derive(Subcommand)]
#[derive(Subcommand, Display)]
pub enum Command {
/// Starts the GraphQL server on the configured port
Start {
Expand Down
15 changes: 15 additions & 0 deletions src/cli/tc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ use std::path::Path;

use anyhow::Result;
use clap::Parser;
use convert_case::{Case, Casing};
use dotenvy::dotenv;
use inquire::Confirm;
use lazy_static::lazy_static;
use stripmargin::StripMargin;

use super::command::{Cli, Command};
Expand All @@ -23,6 +25,9 @@ const FILE_NAME: &str = ".tailcallrc.graphql";
const YML_FILE_NAME: &str = ".graphqlrc.yml";
const JSON_FILE_NAME: &str = ".tailcallrc.schema.json";

lazy_static! {
static ref TRACKER: tailcall_tracker::Tracker = tailcall_tracker::Tracker::default();
}
pub async fn run() -> Result<()> {
if let Ok(path) = dotenv() {
tracing::info!("Env file: {:?} loaded", path);
Expand All @@ -31,6 +36,16 @@ pub async fn run() -> Result<()> {
update_checker::check_for_update().await;
let runtime = cli::runtime::init(&Blueprint::default());
let config_reader = ConfigReader::init(runtime.clone());

// Initialize ping event every 60 seconds
let _ = TRACKER
.init_ping(tokio::time::Duration::from_secs(60))
.await;

// Dispatch the command as an event
let _ = TRACKER
.dispatch(cli.command.to_string().to_case(Case::Snake).as_str())
.await;
match cli.command {
Command::Start { file_paths } => {
let config_module = config_reader.read_all(&file_paths).await?;
Expand Down
17 changes: 17 additions & 0 deletions tailcall-tracker/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "tailcall-tracker"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
reqwest = { version = "0.11", features = ["json", "rustls-tls"] }
anyhow = "1.0.82"
lazy_static = "1.4.0"
serde = { version = "1.0.200", features = ["derive"] }
serde_json = { version = "1.0.82", features = ["preserve_order"] }
machineid-rs = "1.2.4"
tokio = { version = "1.0.1", features = ["rt", "time"] }
tracing = "0.1.40"
sysinfo = "0.30.12"
52 changes: 52 additions & 0 deletions tailcall-tracker/src/check_tracking.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
use std::env;

const LONG_ENV_FILTER_VAR_NAME: &str = "TAILCALL_TRACKER";
const SHORT_ENV_FILTER_VAR_NAME: &str = "TC_TRACKER";
const VERSION: &str = match option_env!("APP_VERSION") {
Some(version) => version,
_ => "0.1.0-dev",
};

/// Checks if tracking is enabled
pub fn check_tracking() -> bool {
let is_prod = !VERSION.contains("dev");
let usage_enabled = env::var(LONG_ENV_FILTER_VAR_NAME)
.or(env::var(SHORT_ENV_FILTER_VAR_NAME))
.map(|v| !v.eq_ignore_ascii_case("false"))
.ok();
check_tracking_inner(is_prod, usage_enabled)
}

fn check_tracking_inner(is_prod_build: bool, tracking_enabled: Option<bool>) -> bool {
if let Some(usage_enabled) = tracking_enabled {
usage_enabled
} else {
is_prod_build
}
}

#[cfg(test)]
mod tests {
use super::*;
#[test]
fn usage_enabled_true() {
assert!(check_tracking_inner(true, Some(true)));
assert!(check_tracking_inner(false, Some(true)));
}

#[test]
fn usage_enabled_false() {
assert!(!check_tracking_inner(true, Some(false)));
assert!(!check_tracking_inner(false, Some(false)));
}

#[test]
fn usage_enabled_none_is_prod_true() {
assert!(check_tracking_inner(true, None));
}

#[test]
fn usage_enabled_none_is_prod_false() {
assert!(!check_tracking_inner(false, None));
}
}
52 changes: 52 additions & 0 deletions tailcall-tracker/src/event.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
use machineid_rs::{Encryption, HWIDComponent, IdBuilder};
use serde::{Deserialize, Serialize};
use sysinfo::System;

const PARAPHRASE: &str = "tc_key";
const DEFAULT_CLIENT_ID: &str = "<anonymous>";

#[derive(Debug, Serialize, Deserialize)]
struct Params {
cpu_cores: String,
os_name: String,
}

#[derive(Debug, Serialize, Deserialize)]
struct EventValue {
name: String,
params: Params,
}

impl EventValue {
fn new(name: &str) -> EventValue {
let sys = System::new_all();
let cores = sys.physical_core_count().unwrap_or(2).to_string();
let os_name = System::long_os_version().unwrap_or("Unknown".to_string());
EventValue {
name: name.to_string(),
params: Params { cpu_cores: cores, os_name },
}
}
}

/// Event structure to be sent to GA
#[derive(Debug, Serialize, Deserialize)]
pub struct Event {
client_id: String,
events: Vec<EventValue>,
}

impl Event {
pub fn new(name: &str) -> Self {
let mut builder = IdBuilder::new(Encryption::SHA256);
builder
.add_component(HWIDComponent::SystemID)
.add_component(HWIDComponent::CPUCores);

let id = builder
.build(PARAPHRASE)
.unwrap_or(DEFAULT_CLIENT_ID.to_string());

Self { client_id: id, events: vec![EventValue::new(name)] }
}
}
4 changes: 4 additions & 0 deletions tailcall-tracker/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
mod check_tracking;
mod event;
mod tracker;
pub use tracker::Tracker;
76 changes: 76 additions & 0 deletions tailcall-tracker/src/tracker.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
use reqwest::header::{HeaderName, HeaderValue};

use crate::check_tracking::check_tracking;
use crate::event::Event;

const API_SECRET: &str = "GVaEzXFeRkCI9YBIylbEjQ";
const MEASUREMENT_ID: &str = "G-JEP3QDWT0G";
const BASE_URL: &str = "https://www.google-analytics.com";

///
/// Base structure to track usage of the CLI application
#[derive(Debug, Clone)]
pub struct Tracker {
base_url: String,
api_secret: String,
measurement_id: String,
is_tracking: bool,
}

impl Default for Tracker {
fn default() -> Self {
Self {
base_url: BASE_URL.to_string(),
api_secret: API_SECRET.to_string(),
measurement_id: MEASUREMENT_ID.to_string(),
is_tracking: check_tracking(),
}
}
}

impl Tracker {
/// Initializes the ping event to be sent after the provided duration
pub async fn init_ping(&'static self, duration: tokio::time::Duration) {
if self.is_tracking {
let mut interval = tokio::time::interval(duration);
tokio::task::spawn(async move {
loop {
interval.tick().await;
let _ = self.dispatch("ping").await;
}
});
}
}

fn create_request(&self, event_name: &str) -> anyhow::Result<reqwest::Request> {
let event = Event::new(event_name);
tracing::debug!("Sending event: {:?}", event);
let mut url = reqwest::Url::parse(self.base_url.as_str())?;
url.set_path("/mp/collect");
url.query_pairs_mut()
.append_pair("api_secret", self.api_secret.as_str())
.append_pair("measurement_id", self.measurement_id.as_str());
let mut request = reqwest::Request::new(reqwest::Method::POST, url);
let header_name = HeaderName::from_static("content-type");
let header_value = HeaderValue::from_str("application/json")?;
request.headers_mut().insert(header_name, header_value);

let _ = request
.body_mut()
.insert(reqwest::Body::from(serde_json::to_string(&event)?));
Ok(request)
}

pub async fn dispatch(&'static self, name: &str) -> anyhow::Result<()> {
if self.is_tracking {
let request = self.create_request(name)?;
let client = reqwest::Client::new();
let response = client.execute(request).await?;
let status = response.status();
let text = response.text().await?;
tracing::debug!("Tracker: {}, message: {:?}", status.as_str(), text);
}

Ok(())
}
}

1 comment on commit 756a56f

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Running 30s test @ http://localhost:8000/graphql

4 threads and 100 connections

Thread Stats Avg Stdev Max +/- Stdev
Latency 7.00ms 3.53ms 253.40ms 76.65%
Req/Sec 3.62k 115.85 3.94k 85.00%

432376 requests in 30.00s, 2.17GB read

Requests/sec: 14410.55

Transfer/sec: 73.96MB

Please sign in to comment.