diff --git a/backend/.sqlx/query-08e4a2dc49c75aa356f3cc75a4abd8fc61409776d641ddb592a4c731e61a0468.json b/backend/.sqlx/query-08e4a2dc49c75aa356f3cc75a4abd8fc61409776d641ddb592a4c731e61a0468.json new file mode 100644 index 0000000000..9da12fd0b8 --- /dev/null +++ b/backend/.sqlx/query-08e4a2dc49c75aa356f3cc75a4abd8fc61409776d641ddb592a4c731e61a0468.json @@ -0,0 +1,26 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT login_type, COUNT(*) FROM password GROUP BY login_type", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "login_type", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "count", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + null + ] + }, + "hash": "08e4a2dc49c75aa356f3cc75a4abd8fc61409776d641ddb592a4c731e61a0468" +} diff --git a/backend/.sqlx/query-1c67cce8cdf6e56752643d6382ebaab76e24581009ca5e8d8b5ce3f199b689ab.json b/backend/.sqlx/query-1c67cce8cdf6e56752643d6382ebaab76e24581009ca5e8d8b5ce3f199b689ab.json new file mode 100644 index 0000000000..8d6a62b4e8 --- /dev/null +++ b/backend/.sqlx/query-1c67cce8cdf6e56752643d6382ebaab76e24581009ca5e8d8b5ce3f199b689ab.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT COUNT(*) FROM worker_ping WHERE ping_at > NOW() - INTERVAL '5 minutes'", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "count", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + null + ] + }, + "hash": "1c67cce8cdf6e56752643d6382ebaab76e24581009ca5e8d8b5ce3f199b689ab" +} diff --git a/backend/src/ee.rs b/backend/src/ee.rs index 1d185b4638..ec4a8a70e7 100644 --- a/backend/src/ee.rs +++ b/backend/src/ee.rs @@ -2,7 +2,8 @@ use windmill_common::error; pub async fn set_license_key(license_key: String) -> anyhow::Result<()> { - use windmill_api::{ee::validate_license_key, LICENSE_KEY, LICENSE_KEY_ID, LICENSE_KEY_VALID}; + use windmill_api::ee::validate_license_key; + use windmill_common::ee::{LICENSE_KEY, LICENSE_KEY_ID, LICENSE_KEY_VALID}; let id = validate_license_key(license_key.clone()).await?; { @@ -24,7 +25,7 @@ pub async fn set_license_key(license_key: String) -> anyhow::Result<()> { #[cfg(feature = "enterprise")] pub async fn verify_license_key() -> error::Result<()> { - use windmill_api::{LICENSE_KEY, LICENSE_KEY_VALID}; + use windmill_common::ee::{LICENSE_KEY, LICENSE_KEY_VALID}; use windmill_common::error::to_anyhow; let expiry_nb = LICENSE_KEY diff --git a/backend/src/monitor.rs b/backend/src/monitor.rs index 6fb1127ca5..ac7c0f55e8 100644 --- a/backend/src/monitor.rs +++ b/backend/src/monitor.rs @@ -41,7 +41,7 @@ use windmill_worker::{ use crate::ee::verify_license_key; #[cfg(feature = "enterprise")] -use windmill_api::LICENSE_KEY_VALID; +use windmill_common::ee::LICENSE_KEY_VALID; use crate::ee::set_license_key; diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 08239168a3..f688149b8e 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -650,6 +650,20 @@ paths: schema: type: string + /settings/send_stats: + post: + summary: send stats + operationId: sendStats + tags: + - setting + responses: + "200": + description: status + content: + text/plain:: + schema: + type: string + /users/email: get: summary: get current user email (if logged in) diff --git a/backend/windmill-api/src/jobs.rs b/backend/windmill-api/src/jobs.rs index 46991e58db..f905119ed6 100644 --- a/backend/windmill-api/src/jobs.rs +++ b/backend/windmill-api/src/jobs.rs @@ -1569,7 +1569,7 @@ async fn check_tag_available_for_workspace(w_id: &str, tag: &Option) -> #[cfg(feature = "enterprise")] pub async fn check_license_key_valid() -> error::Result<()> { - use crate::LICENSE_KEY_VALID; + use windmill_common::ee::LICENSE_KEY_VALID; let valid = *LICENSE_KEY_VALID.read().await; if !valid { diff --git a/backend/windmill-api/src/lib.rs b/backend/windmill-api/src/lib.rs index c3cc827520..9cceff91d2 100644 --- a/backend/windmill-api/src/lib.rs +++ b/backend/windmill-api/src/lib.rs @@ -104,10 +104,6 @@ lazy_static::lazy_static! { connects: HashMap::new(), slack: None })); - - pub static ref LICENSE_KEY_VALID: Arc> = Arc::new(RwLock::new(true)); - pub static ref LICENSE_KEY_ID: Arc> = Arc::new(RwLock::new("".to_string())); - pub static ref LICENSE_KEY: Arc> = Arc::new(RwLock::new("".to_string())); } pub async fn run_server( @@ -337,6 +333,8 @@ async fn ee_license() -> &'static str { #[cfg(feature = "enterprise")] async fn ee_license() -> String { + use windmill_common::ee::LICENSE_KEY_ID; + LICENSE_KEY_ID.read().await.clone() } diff --git a/backend/windmill-api/src/settings.rs b/backend/windmill-api/src/settings.rs index 882e93dbde..ee4de50b18 100644 --- a/backend/windmill-api/src/settings.rs +++ b/backend/windmill-api/src/settings.rs @@ -12,6 +12,7 @@ use crate::{ db::{ApiAuthed, DB}, ee::validate_license_key, utils::require_super_admin, + HTTP_CLIENT, }; use axum::{ @@ -24,7 +25,7 @@ use mail_send::{mail_builder::MessageBuilder, SmtpClientBuilder}; use serde::Deserialize; use tokio::time::timeout; use windmill_common::{ - error::{self, to_anyhow, JsonResult}, + error::{self, to_anyhow, JsonResult, Result}, global_settings::ENV_SETTINGS, server::Smtp, }; @@ -38,6 +39,7 @@ pub fn global_service() -> Router { ) .route("/test_smtp", post(test_email)) .route("/test_license_key", post(test_license_key)) + .route("/send_stats", post(send_stats)) } #[derive(Deserialize)] @@ -158,3 +160,10 @@ pub async fn get_global_setting( Ok(Json(value.unwrap_or_else(|| serde_json::Value::Null))) } + +pub async fn send_stats(Extension(db): Extension, authed: ApiAuthed) -> Result { + require_super_admin(&db, &authed.email).await?; + windmill_common::stats::send_stats(&"manual".to_string(), &HTTP_CLIENT, &db).await?; + + Ok("Sent stats".to_string()) +} diff --git a/backend/windmill-common/src/ee.rs b/backend/windmill-common/src/ee.rs new file mode 100644 index 0000000000..a58cf17e35 --- /dev/null +++ b/backend/windmill-common/src/ee.rs @@ -0,0 +1,8 @@ +use std::sync::Arc; +use tokio::sync::RwLock; + +lazy_static::lazy_static! { + pub static ref LICENSE_KEY_VALID: Arc> = Arc::new(RwLock::new(true)); + pub static ref LICENSE_KEY_ID: Arc> = Arc::new(RwLock::new("".to_string())); + pub static ref LICENSE_KEY: Arc> = Arc::new(RwLock::new("".to_string())); +} diff --git a/backend/windmill-common/src/lib.rs b/backend/windmill-common/src/lib.rs index 1c1097c1cb..d0db6170aa 100644 --- a/backend/windmill-common/src/lib.rs +++ b/backend/windmill-common/src/lib.rs @@ -17,6 +17,7 @@ use sqlx::{Pool, Postgres}; pub mod apps; pub mod db; +pub mod ee; pub mod error; pub mod external_ip; pub mod flow_status; diff --git a/backend/windmill-common/src/stats.rs b/backend/windmill-common/src/stats.rs index e610c42b6f..991aae0aff 100644 --- a/backend/windmill-common/src/stats.rs +++ b/backend/windmill-common/src/stats.rs @@ -2,11 +2,11 @@ use std::str::FromStr; use crate::{ error::{to_anyhow, Result}, - global_settings::{DISABLE_STATS_SETTING, UNIQUE_ID_SETTING}, - utils::GIT_VERSION, + global_settings::DISABLE_STATS_SETTING, + scripts::ScriptLang, + utils::{get_uid, GIT_VERSION}, DB, }; - use chrono::Utc; use cron::Schedule; @@ -74,24 +74,54 @@ pub async fn schedule_stats(db: &DB, instance_name: String, http_client: &reqwes }); } +#[derive(Debug, sqlx::FromRow, serde::Serialize)] +struct JobsUsage { + language: Option, + total_duration: i64, + count: i64, +} + pub async fn send_stats( instance_name: &String, http_client: &reqwest::Client, db: &DB, ) -> Result<()> { - let uid = sqlx::query_scalar!( - "SELECT value FROM global_settings WHERE name = $1", - UNIQUE_ID_SETTING + let uid = get_uid(db).await?; + + let jobs_usage = sqlx::query_as::<_, JobsUsage>( + "SELECT language, COUNT(*) as count, SUM(duration_ms)::BIGINT as total_duration FROM completed_job GROUP BY language", ) - .fetch_one(db) + .fetch_all(db) .await?; - let uid = serde_json::from_value::(uid).map_err(to_anyhow)?; + let login_type_usage = + sqlx::query!("SELECT login_type, COUNT(*) FROM password GROUP BY login_type") + .fetch_all(db) + .await? + .into_iter() + .map(|r| { + serde_json::json!({ + "login_type": r.login_type, + "count": r.count.unwrap_or(0), + }) + }) + .collect::>(); + + let workers_usage = sqlx::query!( + "SELECT COUNT(*) FROM worker_ping WHERE ping_at > NOW() - INTERVAL '5 minutes'" + ) + .fetch_one(db) + .await? + .count + .unwrap_or(0); let payload = serde_json::json!({ "uid": uid, "version": GIT_VERSION, "instance_name": instance_name, + "jobs_usage": jobs_usage, + "login_type_usage": login_type_usage, + "workers_usage": workers_usage, }); let request = http_client diff --git a/backend/windmill-common/src/utils.rs b/backend/windmill-common/src/utils.rs index e81060613e..2d81473fc9 100644 --- a/backend/windmill-common/src/utils.rs +++ b/backend/windmill-common/src/utils.rs @@ -6,6 +6,7 @@ * LICENSE-AGPL for a copy of the license. */ +use crate::ee::LICENSE_KEY_ID; use crate::error::{to_anyhow, Error, Result}; use crate::global_settings::UNIQUE_ID_SETTING; use crate::DB; @@ -103,13 +104,7 @@ pub async fn http_get_from_hub( query_params: Option>, db: &Pool, ) -> Result { - let uid = sqlx::query_scalar!( - "SELECT value FROM global_settings WHERE name = $1", - UNIQUE_ID_SETTING - ) - .fetch_optional(db) - .await? - .map(|v| serde_json::from_value::(v)); + let uid = get_uid(db).await; let mut request = http_client.get(url).header( "Accept", @@ -120,12 +115,10 @@ pub async fn http_get_from_hub( }, ); - if let Some(uid) = uid { - if let Ok(uid) = uid { - request = request.header("X-uid", uid); - } else { - tracing::info!("Invalid uid in global settings: {}", uid.err().unwrap()) - } + if let Ok(uid) = uid { + request = request.header("X-uid", uid); + } else { + tracing::info!("No valid uid found: {}", uid.err().unwrap()) } if let Some(query_params) = query_params { @@ -152,3 +145,20 @@ pub fn calculate_hash(s: &str) -> String { hasher.update(s); format!("{:x}", hasher.finalize()) } + +pub async fn get_uid(db: &DB) -> Result { + let mut uid = LICENSE_KEY_ID.read().await.clone(); + + if uid == "" { + let uid_value = sqlx::query_scalar!( + "SELECT value FROM global_settings WHERE name = $1", + UNIQUE_ID_SETTING + ) + .fetch_one(db) + .await?; + + uid = serde_json::from_value::(uid_value).map_err(to_anyhow)?; + } + + Ok(uid) +} diff --git a/frontend/src/lib/components/InstanceSettings.svelte b/frontend/src/lib/components/InstanceSettings.svelte index ff444f17ce..22f9b955d6 100644 --- a/frontend/src/lib/components/InstanceSettings.svelte +++ b/frontend/src/lib/components/InstanceSettings.svelte @@ -143,6 +143,11 @@ ] let oauth_name = 'custom' + + async function sendStats() { + await SettingService.sendStats() + sendUserToast('Usage sent') + }
@@ -164,8 +169,23 @@
Anonymous usage data is collected to help improve Windmill.
The following information is collected: -
  • version
+
    +
  • version
  • +
  • number and total duration of jobs
  • +
  • login type usage
  • +
  • workers usage
  • +
+ {#if $enterpriseLicense} + + {/if} {/if} {#if category == 'SSO/OAuth'}