1use anyhow::bail;
81use chrono::Utc;
82use futures::StreamExt;
83use mz_adapter::PeekResponseUnary;
84use mz_adapter::telemetry::{EventDetails, SegmentClientExt};
85use mz_ore::collections::CollectionExt;
86use mz_ore::retry::Retry;
87use mz_ore::{soft_panic_or_log, task};
88use mz_repr::adt::jsonb::Jsonb;
89use mz_sql::catalog::EnvironmentId;
90use serde_json::json;
91use tokio::time::{self, Duration};
92
93#[derive(Clone)]
95pub struct Config {
96 pub segment_client: mz_segment::Client,
98 pub adapter_client: mz_adapter::Client,
100 pub environment_id: EnvironmentId,
102 pub license_key: mz_license_keys::ValidatedLicenseKey,
106 pub report_interval: Duration,
108}
109
110pub fn start_reporting(config: Config) {
112 task::spawn(|| "telemetry", report_loop(config));
113}
114
115async fn report_loop(
116 Config {
117 segment_client,
118 adapter_client,
119 environment_id,
120 license_key,
121 report_interval,
122 }: Config,
123) {
124 struct Stats {
125 deletes: u64,
126 inserts: u64,
127 selects: u64,
128 subscribes: u64,
129 updates: u64,
130 }
131
132 let mut last_stats: Option<Stats> = None;
133
134 let mut interval = time::interval(report_interval);
135 loop {
136 interval.tick().await;
137
138 let traits = Retry::default()
139 .initial_backoff(Duration::from_secs(1))
140 .max_tries(5)
141 .retry_async(|_state| async {
142 let active_subscribes = adapter_client
143 .metrics()
144 .active_subscribes
145 .with_label_values(&["user"])
146 .get();
147 let mut rows_stream = adapter_client.support_execute_one(&format!("
148 SELECT jsonb_build_object(
149 'active_aws_privatelink_connections', (SELECT count(*) FROM mz_connections WHERE id LIKE 'u%' AND type = 'aws-privatelink')::int4,
150 'active_clusters', (SELECT count(*) FROM mz_clusters WHERE id LIKE 'u%')::int4,
151 'active_cluster_replicas', (
152 SELECT jsonb_object_agg(base.size, coalesce(count, 0))
153 FROM mz_catalog.mz_cluster_replica_sizes base
154 LEFT JOIN (
155 SELECT r.size, count(*)::int4
156 FROM mz_cluster_replicas r
157 JOIN mz_clusters c ON c.id = r.cluster_id
158 WHERE c.id LIKE 'u%'
159 GROUP BY r.size
160 ) extant ON base.size = extant.size
161 ),
162 'active_confluent_schema_registry_connections', (SELECT count(*) FROM mz_connections WHERE id LIKE 'u%' AND type = 'confluent-schema-registry')::int4,
163 'active_materialized_views', (SELECT count(*) FROM mz_materialized_views WHERE id LIKE 'u%')::int4,
164 'active_sources', (SELECT count(*) FROM mz_sources WHERE id LIKE 'u%' AND type <> 'subsource')::int4,
165 'active_kafka_connections', (SELECT count(*) FROM mz_connections WHERE id LIKE 'u%' AND type = 'kafka')::int4,
166 'active_kafka_sources', (SELECT count(*) FROM mz_sources WHERE id LIKE 'u%' AND type = 'kafka')::int4,
167 'active_load_generator_sources', (SELECT count(*) FROM mz_sources WHERE id LIKE 'u%' AND type = 'load-generator')::int4,
168 'active_postgres_connections', (SELECT count(*) FROM mz_connections WHERE id LIKE 'u%' AND type = 'postgres')::int4,
169 'active_postgres_sources', (SELECT count(*) FROM mz_sources WHERE id LIKE 'u%' AND type = 'postgres')::int4,
170 'active_sinks', (SELECT count(*) FROM mz_sinks WHERE id LIKE 'u%')::int4,
171 'active_ssh_tunnel_connections', (SELECT count(*) FROM mz_connections WHERE id LIKE 'u%' AND type = 'ssh-tunnel')::int4,
172 'active_kafka_sinks', (SELECT count(*) FROM mz_sinks WHERE id LIKE 'u%' AND type = 'kafka')::int4,
173 'active_tables', (SELECT count(*) FROM mz_tables WHERE id LIKE 'u%')::int4,
174 'active_views', (SELECT count(*) FROM mz_views WHERE id LIKE 'u%')::int4,
175 'active_subscribes', {active_subscribes}
176 )",
177 )).await?;
178
179 let mut row_iters = Vec::new();
180
181 while let Some(rows) = rows_stream.next().await {
182 match rows {
183 PeekResponseUnary::Rows(rows) => row_iters.push(rows),
184 PeekResponseUnary::Canceled => bail!("query canceled"),
185 PeekResponseUnary::Error(e) => bail!(e),
186 PeekResponseUnary::DependencyDropped(dep) => {
187 bail!("{}", dep.query_terminated_error())
188 }
189 }
190 }
191
192 let mut rows = Vec::new();
193 for mut row_iter in row_iters {
194 while let Some(row) = row_iter.next() {
195 rows.push(row.to_owned());
196 }
197 }
198
199 assert_eq!(1, rows.len(), "expected one row but got: {:?}", rows);
200 let row = rows.into_first();
201
202 let jsonb = Jsonb::from_row(row);
203 Ok::<_, anyhow::Error>(jsonb.as_ref().to_serde_json())
204 })
205 .await;
206
207 let mut traits = match traits {
208 Ok(traits) => traits,
209 Err(e) => {
210 soft_panic_or_log!("unable to collect telemetry traits: {e}");
211 continue;
212 }
213 };
214
215 let now_secs = u64::try_from(Utc::now().timestamp()).unwrap_or(0);
227 let license_expired = license_key.expired
228 || (license_key.expiration != 0 && now_secs >= license_key.expiration);
229 if let Some(traits) = traits.as_object_mut() {
230 traits.insert("organization_id".into(), json!(license_key.organization));
231 traits.insert("environment_id".into(), json!(license_key.environment_id));
232 traits.insert("license_key_id".into(), json!(license_key.id));
233 traits.insert(
234 "license_expiration_timestamp".into(),
235 json!(license_key.expiration),
236 );
237 traits.insert("license_expired".into(), json!(license_expired));
238 traits.insert(
239 "license_expiration_behavior".into(),
240 json!(license_key.expiration_behavior),
241 );
242 }
243
244 tracing::info!(?traits, "telemetry traits");
245
246 segment_client.group(
247 environment_id.organization_id(),
250 environment_id.organization_id(),
251 json!({
252 environment_id.cloud_provider().to_string(): {
253 environment_id.cloud_provider_region(): traits,
254 }
255 }),
256 );
257
258 let query_total = &adapter_client.metrics().query_total;
259 let current_stats = Stats {
260 deletes: query_total.with_label_values(&["user", "delete"]).get(),
261 inserts: query_total.with_label_values(&["user", "insert"]).get(),
262 updates: query_total.with_label_values(&["user", "update"]).get(),
263 selects: query_total.with_label_values(&["user", "select"]).get(),
264 subscribes: query_total.with_label_values(&["user", "subscribe"]).get(),
265 };
266 if let Some(last_stats) = &last_stats {
267 let mut properties = json!({
268 "deletes": current_stats.deletes - last_stats.deletes,
269 "inserts": current_stats.inserts - last_stats.inserts,
270 "updates": current_stats.updates - last_stats.updates,
271 "selects": current_stats.selects - last_stats.selects,
272 "subscribes": current_stats.subscribes - last_stats.subscribes,
273 });
274 properties
275 .as_object_mut()
276 .unwrap()
277 .extend(traits.as_object().unwrap().clone());
278 segment_client.environment_track(
279 &environment_id,
280 "Environment Rolled Up",
281 properties,
282 EventDetails::default(),
283 );
284 }
285 last_stats = Some(current_stats);
286 }
287}