1use compact_str::ToCompactString;
2use serde::Serialize;
3use std::collections::{BTreeMap, HashMap};
4use utoipa::ToSchema;
5
6nestify::nest! {
7 #[derive(ToSchema, Serialize)] pub struct TelemetryData {
8 pub uuid: uuid::Uuid,
9
10 #[schema(inline)]
11 pub panel: #[derive(ToSchema, Serialize)] pub struct TelemetryDataPanel {
12 pub version: compact_str::CompactString,
13 pub container_type: crate::AppContainerType,
14
15 pub database_version: compact_str::CompactString,
16 pub cache_version: compact_str::CompactString,
17
18 pub architecture: &'static str,
19 pub kernel_version: compact_str::CompactString,
20 },
21
22 #[schema(inline)]
23 pub resources: #[derive(ToSchema, Serialize)] pub struct TelemetryDataResources {
24 #[schema(inline)]
25 pub users: #[derive(ToSchema, Serialize)] pub struct TelemetryDataResourcesUsers {
26 pub total: u64,
27 pub languages: BTreeMap<compact_str::CompactString, u64>,
28 },
29
30 #[schema(inline)]
31 pub backups: #[derive(ToSchema, Serialize)] pub struct TelemetryDataResourcesBackups {
32 pub total: u64,
33 pub disks: HashMap<crate::models::server_backup::BackupDisk, u64>,
34 },
35
36 #[schema(inline)]
37 pub servers: #[derive(ToSchema, Serialize)] pub struct TelemetryDataResourcesServers {
38 pub total: u64,
39 },
40 },
41
42 pub extensions: Vec<crate::extensions::ConstructedExtension>,
43
44 #[schema(inline)]
45 pub nodes: Vec<#[derive(ToSchema, Serialize)] pub struct TelemetryDataNode {
46 pub version: compact_str::CompactString,
47 pub container_type: wings_api::AppContainerType,
48
49 #[schema(inline)]
50 pub memory: wings_api::system_overview::get::Response200Memory,
51 #[schema(inline)]
52 pub servers: wings_api::system_overview::get::Response200Servers,
53
54 pub architecture: compact_str::CompactString,
55 pub kernel_version: compact_str::CompactString,
56 }>,
57 }
58}
59
60impl TelemetryData {
61 pub async fn collect(state: &crate::State) -> Result<Self, anyhow::Error> {
62 let settings = state.settings.get().await?;
63 let uuid = settings.telemetry_uuid.unwrap_or_else(uuid::Uuid::new_v4);
64
65 if settings.telemetry_uuid.is_none() {
66 drop(settings);
67 let mut new_settings = state.settings.get_mut().await?;
68 new_settings.telemetry_uuid = Some(uuid);
69 new_settings.save().await?;
70 } else {
71 drop(settings);
72 }
73
74 let mut node_results = Vec::new();
75 let mut node_page = 1;
76 loop {
77 let nodes = crate::models::node::Node::all_with_pagination(
78 &state.database,
79 node_page,
80 50,
81 None,
82 )
83 .await?;
84 if nodes.data.is_empty() {
85 break;
86 }
87
88 for node in nodes.data {
89 let overview = match node
90 .api_client(&state.database)
91 .await?
92 .get_system_overview()
93 .await
94 {
95 Ok(overview) => overview,
96 Err(_) => continue,
97 };
98
99 node_results.push(TelemetryDataNode {
100 version: overview.version,
101 container_type: overview.container_type,
102 memory: overview.memory,
103 servers: overview.servers,
104 architecture: overview.architecture,
105 kernel_version: overview.kernel_version,
106 });
107 }
108
109 node_page += 1;
110 }
111
112 let user_languages = sqlx::query!(
113 "SELECT users.language, COUNT(*) as count
114 FROM users
115 GROUP BY users.language"
116 )
117 .fetch_all(state.database.read())
118 .await?;
119 let backup_disks = sqlx::query!(
120 r#"SELECT server_backups.disk as "disk: crate::models::server_backup::BackupDisk", COUNT(*) as count
121 FROM server_backups
122 WHERE server_backups.completed IS NOT NULL AND server_backups.deleted IS NULL
123 GROUP BY server_backups.disk"#
124 )
125 .fetch_all(state.database.read())
126 .await?;
127 let servers = sqlx::query!(
128 "SELECT COUNT(*) as count
129 FROM servers"
130 )
131 .fetch_one(state.database.read())
132 .await?;
133
134 Ok(Self {
135 uuid,
136 panel: TelemetryDataPanel {
137 version: state.version.to_compact_string(),
138 container_type: state.container_type,
139 database_version: state.database.version().await?,
140 cache_version: state.cache.version().await?,
141 architecture: std::env::consts::ARCH,
142 kernel_version: sysinfo::System::kernel_long_version().into(),
143 },
144 resources: TelemetryDataResources {
145 users: TelemetryDataResourcesUsers {
146 total: user_languages
147 .iter()
148 .map(|r| r.count.unwrap_or(0) as u64)
149 .sum(),
150 languages: user_languages
151 .into_iter()
152 .map(|r| (r.language.into(), r.count.unwrap_or(0) as u64))
153 .collect(),
154 },
155 backups: TelemetryDataResourcesBackups {
156 total: backup_disks
157 .iter()
158 .map(|r| r.count.unwrap_or(0) as u64)
159 .sum(),
160 disks: backup_disks
161 .into_iter()
162 .map(|r| (r.disk, r.count.unwrap_or(0) as u64))
163 .collect(),
164 },
165 servers: TelemetryDataResourcesServers {
166 total: servers.count.unwrap_or(0) as u64,
167 },
168 },
169 extensions: state.extensions.extensions().await.clone(),
170 nodes: node_results,
171 })
172 }
173}