feat: download encrypted usage (#7804)

This commit is contained in:
hugocasa
2026-02-10 10:50:24 +01:00
committed by GitHub
parent 59cc13de10
commit 4e8a6e6719
10 changed files with 103 additions and 15 deletions

View File

@@ -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"

View File

@@ -1 +1 @@
7596cefdba81482c0b0c0b61be26369f112d8009
7596cefdba81482c0b0c0b61be26369f112d8009

View File

@@ -0,0 +1 @@
-- No-op: cannot restore deleted telemetry data

View File

@@ -0,0 +1,2 @@
-- Delete all saved telemetry data from metrics table
DELETE FROM metrics WHERE id = 'telemetry';

View File

@@ -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

View File

@@ -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,

View File

@@ -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())
}

View File

@@ -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()}

View File

@@ -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

View File

@@ -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': [