feat: download encrypted usage (#7804)
This commit is contained in:
@@ -419,7 +419,7 @@ const_format = { version = "0.2.35", features = ["rust_1_64", "rust_1_51"] }
|
||||
const-str = "0.5"
|
||||
constant_time_eq = "0.3.1"
|
||||
rsa = "^0"
|
||||
aes-gcm = "^0"
|
||||
aes-gcm = "0.10.3"
|
||||
async_zip = { version = "0.0.17", features = ["tokio", "tokio-fs", "deflate", "chrono"] }
|
||||
once_cell = "1.17.1"
|
||||
dashmap = "6.1.0"
|
||||
|
||||
@@ -1 +1 @@
|
||||
7596cefdba81482c0b0c0b61be26369f112d8009
|
||||
7596cefdba81482c0b0c0b61be26369f112d8009
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
-- No-op: cannot restore deleted telemetry data
|
||||
@@ -0,0 +1,2 @@
|
||||
-- Delete all saved telemetry data from metrics table
|
||||
DELETE FROM metrics WHERE id = 'telemetry';
|
||||
@@ -1310,6 +1310,20 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/settings/get_stats:
|
||||
get:
|
||||
summary: get encrypted telemetry stats (EE only)
|
||||
operationId: getStats
|
||||
tags:
|
||||
- setting
|
||||
responses:
|
||||
"200":
|
||||
description: base64-encoded encrypted telemetry blob
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/settings/latest_key_renewal_attempt:
|
||||
get:
|
||||
summary: get latest key renewal attempt
|
||||
|
||||
@@ -58,6 +58,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))
|
||||
.route("/get_stats", get(get_stats))
|
||||
.route(
|
||||
"/latest_key_renewal_attempt",
|
||||
get(get_latest_key_renewal_attempt),
|
||||
@@ -434,6 +435,25 @@ pub async fn send_stats(Extension(db): Extension<DB>, authed: ApiAuthed) -> Resu
|
||||
Ok("Sent stats".to_string())
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
pub async fn get_stats(Extension(db): Extension<DB>, authed: ApiAuthed) -> Result<String> {
|
||||
require_super_admin(&db, &authed.email).await?;
|
||||
let stats = windmill_common::stats_oss::get_stats_payload(
|
||||
&db,
|
||||
&windmill_common::stats_oss::SendStatsReason::Manual,
|
||||
)
|
||||
.await?;
|
||||
let encrypted = windmill_common::stats_oss::encrypt_stats(&stats)?;
|
||||
Ok(encrypted)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
pub async fn get_stats() -> Result<String> {
|
||||
Err(error::Error::BadRequest(
|
||||
"Downloading telemetry is only available on enterprise edition".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct KeyRenewalAttempt {
|
||||
result: String,
|
||||
|
||||
@@ -50,3 +50,19 @@ pub async fn get_user_usage<'c, E: sqlx::Executor<'c, Database = Postgres>>(
|
||||
let usage = ActiveUserUsage { author_count: None, operator_count: None };
|
||||
Ok(usage)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "private"))]
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct Stats {}
|
||||
|
||||
#[cfg(not(feature = "private"))]
|
||||
pub async fn get_stats_payload(_db: &DB, _reason: &SendStatsReason) -> Result<Stats> {
|
||||
// stats details are closed source
|
||||
Ok(Stats {})
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "private"))]
|
||||
pub fn encrypt_stats(_stats: &Stats) -> Result<String> {
|
||||
// stats details are closed source
|
||||
Ok(String::new())
|
||||
}
|
||||
|
||||
@@ -177,7 +177,7 @@
|
||||
{/snippet}
|
||||
|
||||
<!-- {JSON.stringify($values, null, 2)} -->
|
||||
{#if (!setting.cloudonly || isCloudHosted()) && showSetting(setting.key, $values) && !(setting.hiddenIfNull && $values[setting.key] == null) && !(setting.hiddenIfEmpty && !$values[setting.key])}
|
||||
{#if (!setting.cloudonly || isCloudHosted()) && showSetting(setting.key, $values) && !(setting.hiddenIfNull && $values[setting.key] == null) && !(setting.hiddenIfEmpty && !$values[setting.key]) && !(setting.hiddenInEe && $enterpriseLicense)}
|
||||
{#if setting.fieldType == 'select'}
|
||||
<div>
|
||||
{@render LabelSnippet()}
|
||||
|
||||
@@ -231,6 +231,28 @@
|
||||
}
|
||||
}
|
||||
|
||||
let downloadingStats = $state(false)
|
||||
async function downloadStats() {
|
||||
try {
|
||||
downloadingStats = true
|
||||
const encryptedData = await SettingService.getStats()
|
||||
const blob = new Blob([encryptedData], { type: 'application/octet-stream' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `windmill-telemetry-${new Date().toISOString().split('T')[0]}.enc`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
sendUserToast('Telemetry data downloaded')
|
||||
} catch (err) {
|
||||
throw err
|
||||
} finally {
|
||||
downloadingStats = false
|
||||
}
|
||||
}
|
||||
|
||||
function isValidTeamsChannel(value: any): value is TeamsChannel {
|
||||
return (
|
||||
typeof value === 'object' &&
|
||||
@@ -341,18 +363,29 @@
|
||||
<div class="text-primary pb-4 text-xs">
|
||||
On Enterprise Edition, you must send data to check that usage is in line with the
|
||||
terms of the subscription. You can either enable telemetry or regularly send usage
|
||||
data by clicking the button below.
|
||||
data by clicking the button below. For air-gapped instances, you can download the
|
||||
telemetry data and send it manually.
|
||||
</div>
|
||||
<div class="flex gap-2 mb-4">
|
||||
<Button
|
||||
on:click={sendStats}
|
||||
variant="default"
|
||||
btnClasses="w-auto"
|
||||
loading={sendingStats}
|
||||
size="xs"
|
||||
>
|
||||
Send usage
|
||||
</Button>
|
||||
<Button
|
||||
on:click={downloadStats}
|
||||
variant="default"
|
||||
btnClasses="w-auto"
|
||||
loading={downloadingStats}
|
||||
size="xs"
|
||||
>
|
||||
Download usage
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
on:click={sendStats}
|
||||
variant="default"
|
||||
btnClasses="w-auto"
|
||||
wrapperClasses="mb-4"
|
||||
loading={sendingStats}
|
||||
size="xs"
|
||||
>
|
||||
Send usage
|
||||
</Button>
|
||||
{/if}
|
||||
{:else if category == 'Auth/OAuth/SAML'}
|
||||
<AuthSettings
|
||||
|
||||
@@ -58,6 +58,7 @@ export interface Setting {
|
||||
}
|
||||
hiddenIfNull?: boolean
|
||||
hiddenIfEmpty?: boolean
|
||||
hiddenInEe?: boolean
|
||||
requiresReloadOnChange?: boolean
|
||||
isValid?: (value: any) => boolean
|
||||
error?: string
|
||||
@@ -497,7 +498,8 @@ export const settings: Record<string, Setting[]> = {
|
||||
label: 'Disable telemetry',
|
||||
key: 'disable_stats',
|
||||
fieldType: 'boolean',
|
||||
storage: 'setting'
|
||||
storage: 'setting',
|
||||
hiddenInEe: true
|
||||
}
|
||||
],
|
||||
'Secret Storage': [
|
||||
|
||||
Reference in New Issue
Block a user