From 1f9a3eb4031363fc7e01955e2196fd31864d6bf9 Mon Sep 17 00:00:00 2001 From: Nathan <30803215+Nathan903@users.noreply.github.com> Date: Fri, 6 Mar 2026 02:03:12 -0500 Subject: [PATCH 01/27] feat: Introduce an anonymous usage telemetry system with a dedicated server and client-side integration. --- clp-telemetry-server/Cargo.toml | 16 ++ clp-telemetry-server/deployment/Caddyfile | 3 + clp-telemetry-server/deployment/Dockerfile | 11 ++ clp-telemetry-server/docker-compose.yaml | 47 ++++++ clp-telemetry-server/src/main.rs | 156 ++++++++++++++++++ components/api-server/Cargo.toml | 3 + components/api-server/src/bin/api_server.rs | 3 + components/api-server/src/lib.rs | 1 + components/api-server/src/telemetry.rs | 147 +++++++++++++++++ .../clp_package_utils/controller.py | 30 +++- .../clp-py-utils/clp_py_utils/clp_config.py | 5 + .../src/clp_config/package/config.rs | 15 ++ .../src/etc/clp-config.template.json.yaml | 5 + .../src/etc/clp-config.template.text.yaml | 5 + .../package-template/src/sbin/start-clp.sh | 73 ++++++++ docs/src/user-docs/reference-telemetry.md | 103 ++++++++++++ .../package-helm/templates/NOTES.txt | 17 ++ .../templates/api-server-deployment.yaml | 6 + .../package-helm/templates/configmap.yaml | 2 + tools/deployment/package-helm/values.yaml | 5 + 20 files changed, 651 insertions(+), 2 deletions(-) create mode 100644 clp-telemetry-server/Cargo.toml create mode 100644 clp-telemetry-server/deployment/Caddyfile create mode 100644 clp-telemetry-server/deployment/Dockerfile create mode 100644 clp-telemetry-server/docker-compose.yaml create mode 100644 clp-telemetry-server/src/main.rs create mode 100644 components/api-server/src/telemetry.rs create mode 100644 docs/src/user-docs/reference-telemetry.md create mode 100644 tools/deployment/package-helm/templates/NOTES.txt diff --git a/clp-telemetry-server/Cargo.toml b/clp-telemetry-server/Cargo.toml new file mode 100644 index 0000000000..3f38631eb9 --- /dev/null +++ b/clp-telemetry-server/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "clp-telemetry-server" +version = "0.1.0" +edition = "2024" + +[dependencies] +axum = { version = "0.8", features = ["json"] } +chrono = { version = "0.4", features = ["serde"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "chrono", "uuid"] } +tokio = { version = "1", features = ["full"] } +tower-http = { version = "0.6", features = ["limit"] } +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +uuid = { version = "1", features = ["v4", "serde"] } diff --git a/clp-telemetry-server/deployment/Caddyfile b/clp-telemetry-server/deployment/Caddyfile new file mode 100644 index 0000000000..a35f312d39 --- /dev/null +++ b/clp-telemetry-server/deployment/Caddyfile @@ -0,0 +1,3 @@ +telemetry.yscope.io { + reverse_proxy telemetry-server:8080 +} diff --git a/clp-telemetry-server/deployment/Dockerfile b/clp-telemetry-server/deployment/Dockerfile new file mode 100644 index 0000000000..5096df6218 --- /dev/null +++ b/clp-telemetry-server/deployment/Dockerfile @@ -0,0 +1,11 @@ +FROM rust:1.85 AS builder +WORKDIR /app +COPY Cargo.toml Cargo.lock* ./ +COPY src ./src +RUN cargo build --release + +FROM debian:bookworm-slim +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && rm -rf /var/lib/apt/lists/* +COPY --from=builder /app/target/release/clp-telemetry-server /usr/local/bin/ +EXPOSE 8080 +CMD ["clp-telemetry-server"] diff --git a/clp-telemetry-server/docker-compose.yaml b/clp-telemetry-server/docker-compose.yaml new file mode 100644 index 0000000000..10b21ec03c --- /dev/null +++ b/clp-telemetry-server/docker-compose.yaml @@ -0,0 +1,47 @@ +services: + telemetry-server: + build: + context: . + dockerfile: deployment/Dockerfile + environment: + - DATABASE_URL=postgres://telemetry:telemetry@postgres:5432/telemetry + - RUST_LOG=info + ports: + - "8080:8080" + depends_on: + postgres: + condition: service_healthy + restart: unless-stopped + + postgres: + image: postgres:16-alpine + environment: + POSTGRES_USER: telemetry + POSTGRES_PASSWORD: telemetry + POSTGRES_DB: telemetry + volumes: + - pgdata:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U telemetry"] + interval: 5s + timeout: 5s + retries: 5 + restart: unless-stopped + + caddy: + image: caddy:2-alpine + ports: + - "80:80" + - "443:443" + volumes: + - ./deployment/Caddyfile:/etc/caddy/Caddyfile:ro + - caddy_data:/data + - caddy_config:/config + depends_on: + - telemetry-server + restart: unless-stopped + +volumes: + pgdata: + caddy_data: + caddy_config: diff --git a/clp-telemetry-server/src/main.rs b/clp-telemetry-server/src/main.rs new file mode 100644 index 0000000000..18286c0a24 --- /dev/null +++ b/clp-telemetry-server/src/main.rs @@ -0,0 +1,156 @@ +use std::net::SocketAddr; +use std::sync::Arc; + +use axum::extract::State; +use axum::http::StatusCode; +use axum::routing::post; +use axum::{Json, Router}; +use serde::{Deserialize, Serialize}; +use sqlx::PgPool; +use tower_http::limit::RequestBodyLimitLayer; + +/// Maximum request body size (10 KB). +const MAX_BODY_SIZE: usize = 10 * 1024; + +#[derive(Debug, Deserialize, Serialize)] +struct TelemetryEvent { + schema_version: u32, + telemetry_id: String, + timestamp: String, + event_type: String, + #[serde(default)] + clp_version: String, + #[serde(default)] + deployment_method: String, + #[serde(default)] + os: String, + #[serde(default)] + os_version: String, + #[serde(default)] + arch: String, + #[serde(default)] + storage_engine: String, + #[serde(default)] + payload: Option, +} + +struct AppState { + db: PgPool, +} + +async fn ingest_event( + State(state): State>, + Json(event): Json, +) -> StatusCode { + // Validate required fields + if event.telemetry_id.is_empty() || event.event_type.is_empty() { + return StatusCode::BAD_REQUEST; + } + + // Validate telemetry_id is a plausible UUID (basic check) + if event.telemetry_id.len() > 64 { + return StatusCode::BAD_REQUEST; + } + + let payload_json = event + .payload + .as_ref() + .map(|p| serde_json::to_string(p).unwrap_or_default()); + + let result = sqlx::query( + r#" + INSERT INTO telemetry_events ( + schema_version, telemetry_id, event_timestamp, event_type, + clp_version, deployment_method, os, os_version, arch, + storage_engine, payload + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) + "#, + ) + .bind(event.schema_version as i32) + .bind(&event.telemetry_id) + .bind(&event.timestamp) + .bind(&event.event_type) + .bind(&event.clp_version) + .bind(&event.deployment_method) + .bind(&event.os) + .bind(&event.os_version) + .bind(&event.arch) + .bind(&event.storage_engine) + .bind(payload_json) + .execute(&state.db) + .await; + + match result { + Ok(_) => StatusCode::CREATED, + Err(e) => { + tracing::error!("Failed to insert telemetry event: {e}"); + StatusCode::INTERNAL_SERVER_ERROR + } + } +} + +async fn health() -> StatusCode { + StatusCode::OK +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| "info".into()), + ) + .init(); + + let database_url = + std::env::var("DATABASE_URL").expect("DATABASE_URL environment variable must be set"); + + let db = PgPool::connect(&database_url).await?; + + // Run migrations on startup + sqlx::query( + r#" + CREATE TABLE IF NOT EXISTS telemetry_events ( + id BIGSERIAL PRIMARY KEY, + schema_version INTEGER NOT NULL, + telemetry_id TEXT NOT NULL, + event_timestamp TEXT NOT NULL, + event_type TEXT NOT NULL, + clp_version TEXT NOT NULL DEFAULT '', + deployment_method TEXT NOT NULL DEFAULT '', + os TEXT NOT NULL DEFAULT '', + os_version TEXT NOT NULL DEFAULT '', + arch TEXT NOT NULL DEFAULT '', + storage_engine TEXT NOT NULL DEFAULT '', + payload TEXT, + received_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + ); + + CREATE INDEX IF NOT EXISTS idx_telemetry_events_telemetry_id + ON telemetry_events (telemetry_id); + CREATE INDEX IF NOT EXISTS idx_telemetry_events_event_type + ON telemetry_events (event_type); + CREATE INDEX IF NOT EXISTS idx_telemetry_events_received_at + ON telemetry_events (received_at); + "#, + ) + .execute(&db) + .await?; + + let state = Arc::new(AppState { db }); + + let app = Router::new() + .route("/v1/events", post(ingest_event)) + .route("/health", axum::routing::get(health)) + .layer(RequestBodyLimitLayer::new(MAX_BODY_SIZE)) + .with_state(state); + + let addr: SocketAddr = "0.0.0.0:8080".parse()?; + tracing::info!("Telemetry server listening on {addr}"); + + let listener = tokio::net::TcpListener::bind(&addr).await?; + axum::serve(listener, app).await?; + + Ok(()) +} diff --git a/components/api-server/Cargo.toml b/components/api-server/Cargo.toml index 1dd910d41c..2a2a2010b4 100644 --- a/components/api-server/Cargo.toml +++ b/components/api-server/Cargo.toml @@ -36,5 +36,8 @@ thiserror = "2.0.18" tokio = { version = "1.49.0", features = ["full"] } tower-http = { version = "0.6.8", features = ["cors"] } tracing = "0.1.44" +uuid = { version = "1", features = ["v4"] } utoipa = { version = "5.4.0", features = ["axum_extras"] } utoipa-axum = "0.2.0" +reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } +chrono = { version = "0.4", features = ["serde"] } diff --git a/components/api-server/src/bin/api_server.rs b/components/api-server/src/bin/api_server.rs index 05a9126e5e..cac7936e96 100644 --- a/components/api-server/src/bin/api_server.rs +++ b/components/api-server/src/bin/api_server.rs @@ -70,6 +70,9 @@ async fn main() -> anyhow::Result<()> { .await .context("Cannot connect to CLP")?; + // Spawn telemetry background task (non-blocking, failures are silent) + tokio::spawn(api_server::telemetry::run_telemetry_loop(config)); + let router = api_server::routes::from_client(client)?; tracing::info!("Server started at {addr}"); diff --git a/components/api-server/src/lib.rs b/components/api-server/src/lib.rs index 072cb6e073..c911bb3536 100644 --- a/components/api-server/src/lib.rs +++ b/components/api-server/src/lib.rs @@ -1,3 +1,4 @@ pub mod client; mod error; pub mod routes; +pub mod telemetry; diff --git a/components/api-server/src/telemetry.rs b/components/api-server/src/telemetry.rs new file mode 100644 index 0000000000..8d3a2bfd73 --- /dev/null +++ b/components/api-server/src/telemetry.rs @@ -0,0 +1,147 @@ +use std::env; +use std::time::Duration; + +use chrono::Utc; +use serde::Serialize; + +use clp_rust_utils::clp_config::package::config::Config; + +const TELEMETRY_ENDPOINT: &str = "https://telemetry.yscope.io/v1/events"; +const TELEMETRY_SEND_INTERVAL: Duration = Duration::from_secs(24 * 60 * 60); // 24 hours +const TELEMETRY_HTTP_TIMEOUT: Duration = Duration::from_secs(5); + +/// Schema version for the telemetry payload. +const SCHEMA_VERSION: u32 = 1; + +#[derive(Debug, Serialize)] +struct TelemetryEvent { + schema_version: u32, + telemetry_id: String, + timestamp: String, + event_type: String, + clp_version: String, + deployment_method: String, + os: String, + os_version: String, + arch: String, + storage_engine: String, + #[serde(skip_serializing_if = "Option::is_none")] + payload: Option, +} + +/// Checks whether telemetry is disabled through any of the supported mechanisms. +fn is_telemetry_disabled(config: &Config) -> bool { + // Check config file setting + if config.telemetry.disable { + return true; + } + + // Check CLP_DISABLE_TELEMETRY env var + if let Ok(val) = env::var("CLP_DISABLE_TELEMETRY") { + if val.eq_ignore_ascii_case("true") || val == "1" { + return true; + } + } + + // Check DO_NOT_TRACK env var (https://consoledonottrack.com/) + if let Ok(val) = env::var("DO_NOT_TRACK") { + if val == "1" { + return true; + } + } + + false +} + +fn get_storage_engine_str(config: &Config) -> &'static str { + match config.package.storage_engine { + clp_rust_utils::clp_config::package::config::StorageEngine::Clp => "clp", + clp_rust_utils::clp_config::package::config::StorageEngine::ClpS => "clp-s", + } +} + +fn build_event(event_type: &str, config: &Config) -> TelemetryEvent { + let telemetry_id = env::var("CLP_INSTANCE_ID").unwrap_or_else(|_| "unknown".to_owned()); + let clp_version = env::var("CLP_VERSION").unwrap_or_else(|_| "unknown".to_owned()); + let deployment_method = + env::var("CLP_DEPLOYMENT_METHOD").unwrap_or_else(|_| "unknown".to_owned()); + let os = env::var("CLP_HOST_OS").unwrap_or_else(|_| std::env::consts::OS.to_owned()); + let os_version = env::var("CLP_HOST_OS_VERSION").unwrap_or_else(|_| "unknown".to_owned()); + let arch = env::var("CLP_HOST_ARCH").unwrap_or_else(|_| std::env::consts::ARCH.to_owned()); + + TelemetryEvent { + schema_version: SCHEMA_VERSION, + telemetry_id, + timestamp: Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string(), + event_type: event_type.to_owned(), + clp_version, + deployment_method, + os, + os_version, + arch, + storage_engine: get_storage_engine_str(config).to_owned(), + payload: None, + } +} + +async fn send_event(client: &reqwest::Client, event: &TelemetryEvent) { + let is_debug = env::var("CLP_TELEMETRY_DEBUG") + .map(|v| v.eq_ignore_ascii_case("true") || v == "1") + .unwrap_or(false); + + if is_debug { + match serde_json::to_string_pretty(event) { + Ok(json) => tracing::info!("[telemetry-debug] Would send:\n{json}"), + Err(e) => tracing::warn!("[telemetry-debug] Failed to serialize event: {e}"), + } + return; + } + + match client.post(TELEMETRY_ENDPOINT).json(event).send().await { + Ok(resp) => { + tracing::debug!("Telemetry event sent, status: {}", resp.status()); + } + Err(e) => { + tracing::debug!("Failed to send telemetry event (this is not an error): {e}"); + } + } +} + +/// Runs the telemetry background loop. Sends a `deployment_start` event on startup +/// and a `heartbeat` event every 24 hours. All failures are silently ignored. +/// +/// This function is designed to be spawned as a background tokio task: +/// ```ignore +/// tokio::spawn(telemetry::run_telemetry_loop(config)); +/// ``` +pub async fn run_telemetry_loop(config: Config) { + if is_telemetry_disabled(&config) { + tracing::info!("Anonymous telemetry is disabled."); + return; + } + + tracing::info!("Anonymous telemetry is enabled. Set CLP_DISABLE_TELEMETRY=true to disable."); + + let client = match reqwest::Client::builder() + .timeout(TELEMETRY_HTTP_TIMEOUT) + .build() + { + Ok(c) => c, + Err(e) => { + tracing::debug!("Failed to create telemetry HTTP client: {e}"); + return; + } + }; + + // Send deployment_start event + let start_event = build_event("deployment_start", &config); + send_event(&client, &start_event).await; + + // Periodic heartbeat + loop { + tokio::time::sleep(TELEMETRY_SEND_INTERVAL).await; + + let heartbeat_event = build_event("heartbeat", &config); + send_event(&client, &heartbeat_event).await; + } +} diff --git a/components/clp-package-utils/clp_package_utils/controller.py b/components/clp-package-utils/clp_package_utils/controller.py index d7a0c0b99e..64a149a344 100644 --- a/components/clp-package-utils/clp_package_utils/controller.py +++ b/components/clp-package-utils/clp_package_utils/controller.py @@ -645,6 +645,32 @@ def _set_up_env_for_api_server(self) -> EnvVarsDict: "CLP_API_SERVER_PORT": str(self._clp_config.api_server.port), } + # Telemetry env vars + instance_id_file = self._clp_config.logs_directory / "instance-id" + resolved_id_file = resolve_host_path_in_container(instance_id_file) + if resolved_id_file.exists(): + with open(resolved_id_file, "r") as f: + env_vars["CLP_INSTANCE_ID"] = f.readline().strip() + + version_file = resolve_host_path_in_container(self._clp_config._version_file_path) + if version_file.exists(): + with open(version_file, "r") as f: + env_vars["CLP_VERSION"] = f.read().strip() + + env_vars["CLP_DEPLOYMENT_METHOD"] = "docker-compose" + + # Pass through host OS info (set by start-clp.sh) + for var in ( + "CLP_HOST_OS", + "CLP_HOST_OS_VERSION", + "CLP_HOST_ARCH", + "CLP_DISABLE_TELEMETRY", + "CLP_TELEMETRY_DEBUG", + ): + val = os.environ.get(var) + if val is not None: + env_vars[var] = val + return env_vars def _set_up_env_for_log_ingestor(self) -> EnvVarsDict: @@ -964,7 +990,7 @@ def __init__( self, clp_config: ClpConfig, instance_id: str, restart_policy: str = "on-failure:3" ) -> None: """Initializes the DockerComposeController.""" - self._project_name = f"clp-package-{instance_id}" + self._project_name = f"clp-package-{instance_id[-4:]}" self._restart_policy = restart_policy super().__init__(clp_config) @@ -1155,7 +1181,7 @@ def get_or_create_instance_id(clp_config: ClpConfig) -> str: with open(resolved_instance_id_file_path, "r") as f: instance_id = f.readline() else: - instance_id = str(uuid.uuid4())[-4:] + instance_id = str(uuid.uuid4()) with open(resolved_instance_id_file_path, "w") as f: f.write(instance_id) diff --git a/components/clp-py-utils/clp_py_utils/clp_config.py b/components/clp-py-utils/clp_py_utils/clp_config.py index 42a9623c2b..13c50d462e 100644 --- a/components/clp-py-utils/clp_py_utils/clp_config.py +++ b/components/clp-py-utils/clp_py_utils/clp_config.py @@ -802,6 +802,10 @@ def _get_env_var(name: str) -> str: return value +class Telemetry(BaseModel): + disable: bool = False + + class ClpConfig(BaseModel): container_image_ref: NonEmptyStr | None = None @@ -840,6 +844,7 @@ class ClpConfig(BaseModel): logs_directory: SerializablePath = CLP_DEFAULT_LOG_DIRECTORY_PATH tmp_directory: SerializablePath = CLP_DEFAULT_TMP_DIRECTORY_PATH aws_config_directory: SerializablePath | None = None + telemetry: Telemetry = Telemetry() _container_image_id_path: SerializablePath = PrivateAttr( default=CLP_PACKAGE_CONTAINER_IMAGE_ID_PATH diff --git a/components/clp-rust-utils/src/clp_config/package/config.rs b/components/clp-rust-utils/src/clp_config/package/config.rs index 4ccf67039d..fa5bba60c3 100644 --- a/components/clp-rust-utils/src/clp_config/package/config.rs +++ b/components/clp-rust-utils/src/clp_config/package/config.rs @@ -21,6 +21,7 @@ pub struct Config { pub stream_output: StreamOutput, pub logs_input: LogsInput, pub archive_output: ArchiveOutput, + pub telemetry: Telemetry, } impl Default for Config { @@ -37,6 +38,7 @@ impl Default for Config { config: FsIngestion::default(), }, archive_output: ArchiveOutput::default(), + telemetry: Telemetry::default(), } } } @@ -316,6 +318,19 @@ pub enum LogsInput { }, } +/// Mirror of `clp_py_utils.clp_config.Telemetry`. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq)] +#[serde(default)] +pub struct Telemetry { + pub disable: bool, +} + +impl Default for Telemetry { + fn default() -> Self { + Self { disable: false } + } +} + #[cfg(test)] mod tests { use super::LogsInput; diff --git a/components/package-template/src/etc/clp-config.template.json.yaml b/components/package-template/src/etc/clp-config.template.json.yaml index 896b69a426..1bb5c50878 100644 --- a/components/package-template/src/etc/clp-config.template.json.yaml +++ b/components/package-template/src/etc/clp-config.template.json.yaml @@ -182,3 +182,8 @@ # ## Location of the AWS tools' config files (e.g., `~/.aws`) #aws_config_directory: null +# +## Anonymous usage telemetry. Set to true to disable. +## See: https://docs.yscope.com/clp/main/user-guide/telemetry +#telemetry: +# disable: false diff --git a/components/package-template/src/etc/clp-config.template.text.yaml b/components/package-template/src/etc/clp-config.template.text.yaml index d08ba2da52..a10158428e 100644 --- a/components/package-template/src/etc/clp-config.template.text.yaml +++ b/components/package-template/src/etc/clp-config.template.text.yaml @@ -165,3 +165,8 @@ log_ingestor: null # ## Location of the AWS tools' config files (e.g., `~/.aws`) #aws_config_directory: null +# +## Anonymous usage telemetry. Set to true to disable. +## See: https://docs.yscope.com/clp/main/user-guide/telemetry +#telemetry: +# disable: false diff --git a/components/package-template/src/sbin/start-clp.sh b/components/package-template/src/sbin/start-clp.sh index bd7a3ca76c..7434e889d6 100755 --- a/components/package-template/src/sbin/start-clp.sh +++ b/components/package-template/src/sbin/start-clp.sh @@ -10,8 +10,81 @@ common_env_path="$script_dir/.common-env.sh" # shellcheck source=.common-env.sh source "$common_env_path" +# --- Telemetry consent prompt --- +# Determines whether to show the first-run telemetry consent prompt. +# The prompt is skipped if: +# 1. CLP_DISABLE_TELEMETRY or DO_NOT_TRACK env vars are set +# 2. telemetry.disable is explicitly set in clp-config.yaml +# 3. instance-id file already exists (not a first run) +# If shown and the user declines, telemetry.disable is written to clp-config.yaml. + +telemetry_prompt_needed=false + +# Find the config file path (mirror the default in start_clp.py) +clp_config_path="${CLP_HOME}/etc/clp-config.yaml" + +# Check env vars first +if [[ "${CLP_DISABLE_TELEMETRY:-}" == "true" ]] || [[ "${CLP_DISABLE_TELEMETRY:-}" == "1" ]]; then + telemetry_prompt_needed=false +elif [[ "${DO_NOT_TRACK:-}" == "1" ]]; then + telemetry_prompt_needed=false +# Check if telemetry is already configured in the config file +elif [[ -f "$clp_config_path" ]] && grep -q "telemetry:" "$clp_config_path" 2>/dev/null; then + telemetry_prompt_needed=false +# Check if instance-id exists (not first run) +elif [[ -f "${CLP_HOME}/var/log/instance-id" ]]; then + telemetry_prompt_needed=false +else + telemetry_prompt_needed=true +fi + +if [[ "$telemetry_prompt_needed" == "true" ]]; then + if [[ -t 0 ]]; then + # Interactive: show the consent prompt + echo "================================================================================" + echo "CLP collects anonymous usage telemetry to help improve the software." + echo "This includes: CLP version, OS/architecture, deployment method, and" + echo "component health status. It does NOT include: log content, queries," + echo "hostnames, IP addresses, or any personally identifiable" + echo "information." + echo "" + echo "Telemetry is sent to: https://telemetry.yscope.io" + echo "For details, see: https://docs.yscope.com/clp/main/user-guide/telemetry" + echo "" + echo "You can disable telemetry at any time by setting CLP_DISABLE_TELEMETRY=true" + echo "or by blocking https://telemetry.yscope.io at the network level." + echo "" + read -r -p "Enable anonymous telemetry to help improve CLP? [Y/n] " telemetry_response + echo "================================================================================" + + if [[ "$telemetry_response" =~ ^[Nn]$ ]]; then + # User opted out — persist to config + if [[ -f "$clp_config_path" ]]; then + echo "" >> "$clp_config_path" + echo "telemetry:" >> "$clp_config_path" + echo " disable: true" >> "$clp_config_path" + else + printf "telemetry:\n disable: true\n" > "$clp_config_path" + fi + echo "Telemetry has been disabled. You can re-enable it in ${clp_config_path}." + fi + fi + # Non-interactive: default to enabled (no prompt, no config write needed) +fi + +# --- Export host OS info for telemetry --- +export CLP_HOST_OS="linux" +if [[ -f /etc/os-release ]]; then + CLP_HOST_OS_VERSION="$(. /etc/os-release && echo "${ID:-unknown}-${VERSION_ID:-unknown}")" +else + CLP_HOST_OS_VERSION="unknown" +fi +export CLP_HOST_OS_VERSION +export CLP_HOST_ARCH="$(uname -m)" + docker compose -f "$CLP_HOME/docker-compose.runtime.yaml" \ run --rm "${CLP_COMPOSE_RUN_EXTRA_FLAGS[@]}" clp-runtime \ python3 \ -m clp_package_utils.scripts.start_clp \ "$@" + diff --git a/docs/src/user-docs/reference-telemetry.md b/docs/src/user-docs/reference-telemetry.md new file mode 100644 index 0000000000..1570d5d574 --- /dev/null +++ b/docs/src/user-docs/reference-telemetry.md @@ -0,0 +1,103 @@ +# Telemetry + +CLP collects anonymous usage telemetry to help improve the software. Telemetry is **enabled +by default** and can be easily disabled through multiple mechanisms. + +## Why we collect telemetry + +As an open-source project, we have limited visibility into how CLP is used in the community. +Anonymous telemetry helps us: + +- Understand how many deployments exist and what versions are running +- Identify common deployment methods and platform preferences +- Prioritize development and build target decisions +- Detect common failure modes + +## What we collect + +| Data point | Example | Purpose | +|---|---|---| +| Anonymous installation UUID | `550e8400-e29b-41d4-a716-446655440000` | Deduplicate events from the same deployment | +| CLP version | `0.9.1` | Track version adoption | +| Deployment method | `docker-compose` / `helm` | Understand deployment preferences | +| OS type and version | `linux`, `ubuntu-22.04` | Inform platform support | +| CPU architecture | `x86_64`, `aarch64` | Inform build target priorities | +| Storage engine | `clp-s` / `clp` | Track feature adoption | +| Event type | `deployment_start`, `heartbeat` | Understand usage patterns | +| Timestamp (UTC) | `2025-01-15T10:30:00Z` | Time-series analysis | + +## What we do NOT collect + +- **Log content or query content** — never, under any circumstances +- **Personally identifiable information** — no usernames, emails, or organization names +- **IP addresses** — visible during network communication but **not logged or stored** +- **Credentials and secrets** — no passwords, API keys, or connection strings +- **Hostnames** — no server or container hostnames + +## Telemetry endpoint + +All telemetry data is sent to: `https://telemetry.yscope.io` + +## How to disable telemetry + +Any **one** of the following methods is sufficient: + +### Environment variable + +```bash +export CLP_DISABLE_TELEMETRY=true +``` + +Or use the [Console Do Not Track](https://consoledonottrack.com/) standard: + +```bash +export DO_NOT_TRACK=1 +``` + +### Configuration file + +Add to your `clp-config.yaml`: + +```yaml +telemetry: + disable: true +``` + +### First-run prompt + +When running `start-clp.sh` for the first time in an interactive terminal, you will see a +consent prompt. Answering `n` will automatically set `telemetry.disable: true` in your config. + +### Helm chart + +Set in your `values.yaml`: + +```yaml +clpConfig: + telemetry: + disable: true +``` + +### Network-level blocking + +Block `https://telemetry.yscope.io` at your firewall or proxy. This is the simplest way to +disable telemetry for an entire organization. + +## Debug mode + +To inspect exactly what telemetry data would be sent without actually sending it: + +```bash +export CLP_TELEMETRY_DEBUG=true +``` + +The JSON payload will be logged to the API server log file instead of being transmitted. + +## Source code + +The telemetry implementation is fully open source: + +- **Client**: `components/api-server/src/telemetry.rs` +- **Consent prompt**: `components/package-template/src/sbin/start-clp.sh` +- **Configuration**: `telemetry.disable` in `clp-config.yaml` +- **Server**: `clp-telemetry-server/` diff --git a/tools/deployment/package-helm/templates/NOTES.txt b/tools/deployment/package-helm/templates/NOTES.txt new file mode 100644 index 0000000000..38e555816a --- /dev/null +++ b/tools/deployment/package-helm/templates/NOTES.txt @@ -0,0 +1,17 @@ +================================================================================ +CLP collects anonymous usage telemetry to help improve the software. +This includes: CLP version, OS/architecture, deployment method, and +component health status. It does NOT include: log content, queries, +hostnames, IP addresses, or any personally identifiable information. + +Telemetry is sent to: https://telemetry.yscope.io +For details, see: https://docs.yscope.com/clp/main/user-guide/telemetry + +To disable telemetry, update your values.yaml: + + clpConfig: + telemetry: + disable: true + +Or set the CLP_DISABLE_TELEMETRY=true environment variable. +================================================================================ diff --git a/tools/deployment/package-helm/templates/api-server-deployment.yaml b/tools/deployment/package-helm/templates/api-server-deployment.yaml index 01d734323b..626e5b67d9 100644 --- a/tools/deployment/package-helm/templates/api-server-deployment.yaml +++ b/tools/deployment/package-helm/templates/api-server-deployment.yaml @@ -48,6 +48,12 @@ spec: key: "username" - name: "RUST_LOG" value: "INFO" + - name: "CLP_DEPLOYMENT_METHOD" + value: "helm" + {{- if .Values.clpConfig.telemetry.disable }} + - name: "CLP_DISABLE_TELEMETRY" + value: "true" + {{- end }} ports: - name: "api-server" containerPort: 3001 diff --git a/tools/deployment/package-helm/templates/configmap.yaml b/tools/deployment/package-helm/templates/configmap.yaml index aefcab1ade..55d2dd7f31 100644 --- a/tools/deployment/package-helm/templates/configmap.yaml +++ b/tools/deployment/package-helm/templates/configmap.yaml @@ -219,6 +219,8 @@ data: {{- else }} mcp_server: null {{- end }} + telemetry: + disable: {{ .Values.clpConfig.telemetry.disable }} mysql-logging.cnf: | [mysqld] diff --git a/tools/deployment/package-helm/values.yaml b/tools/deployment/package-helm/values.yaml index d8e0d07ec9..ed3bb26e74 100644 --- a/tools/deployment/package-helm/values.yaml +++ b/tools/deployment/package-helm/values.yaml @@ -169,6 +169,11 @@ clpConfig: # port: 30800 # logging_level: "INFO" + # Anonymous usage telemetry. Set to true to disable. + # See: https://docs.yscope.com/clp/main/user-guide/telemetry + telemetry: + disable: false + # log-ingestor config. Currently, the config is applicable only if `logs_input.type` is "s3". log_ingestor: port: 30302 From fe9c572b6979d26f50a38eb2948b280a89342060 Mon Sep 17 00:00:00 2001 From: Nathan <30803215+Nathan903@users.noreply.github.com> Date: Fri, 6 Mar 2026 02:30:40 -0500 Subject: [PATCH 02/27] remove server code --- clp-telemetry-server/Cargo.toml | 16 --- clp-telemetry-server/deployment/Caddyfile | 3 - clp-telemetry-server/deployment/Dockerfile | 11 -- clp-telemetry-server/docker-compose.yaml | 47 ------- clp-telemetry-server/src/main.rs | 156 --------------------- 5 files changed, 233 deletions(-) delete mode 100644 clp-telemetry-server/Cargo.toml delete mode 100644 clp-telemetry-server/deployment/Caddyfile delete mode 100644 clp-telemetry-server/deployment/Dockerfile delete mode 100644 clp-telemetry-server/docker-compose.yaml delete mode 100644 clp-telemetry-server/src/main.rs diff --git a/clp-telemetry-server/Cargo.toml b/clp-telemetry-server/Cargo.toml deleted file mode 100644 index 3f38631eb9..0000000000 --- a/clp-telemetry-server/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "clp-telemetry-server" -version = "0.1.0" -edition = "2024" - -[dependencies] -axum = { version = "0.8", features = ["json"] } -chrono = { version = "0.4", features = ["serde"] } -serde = { version = "1", features = ["derive"] } -serde_json = "1" -sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "chrono", "uuid"] } -tokio = { version = "1", features = ["full"] } -tower-http = { version = "0.6", features = ["limit"] } -tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } -uuid = { version = "1", features = ["v4", "serde"] } diff --git a/clp-telemetry-server/deployment/Caddyfile b/clp-telemetry-server/deployment/Caddyfile deleted file mode 100644 index a35f312d39..0000000000 --- a/clp-telemetry-server/deployment/Caddyfile +++ /dev/null @@ -1,3 +0,0 @@ -telemetry.yscope.io { - reverse_proxy telemetry-server:8080 -} diff --git a/clp-telemetry-server/deployment/Dockerfile b/clp-telemetry-server/deployment/Dockerfile deleted file mode 100644 index 5096df6218..0000000000 --- a/clp-telemetry-server/deployment/Dockerfile +++ /dev/null @@ -1,11 +0,0 @@ -FROM rust:1.85 AS builder -WORKDIR /app -COPY Cargo.toml Cargo.lock* ./ -COPY src ./src -RUN cargo build --release - -FROM debian:bookworm-slim -RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && rm -rf /var/lib/apt/lists/* -COPY --from=builder /app/target/release/clp-telemetry-server /usr/local/bin/ -EXPOSE 8080 -CMD ["clp-telemetry-server"] diff --git a/clp-telemetry-server/docker-compose.yaml b/clp-telemetry-server/docker-compose.yaml deleted file mode 100644 index 10b21ec03c..0000000000 --- a/clp-telemetry-server/docker-compose.yaml +++ /dev/null @@ -1,47 +0,0 @@ -services: - telemetry-server: - build: - context: . - dockerfile: deployment/Dockerfile - environment: - - DATABASE_URL=postgres://telemetry:telemetry@postgres:5432/telemetry - - RUST_LOG=info - ports: - - "8080:8080" - depends_on: - postgres: - condition: service_healthy - restart: unless-stopped - - postgres: - image: postgres:16-alpine - environment: - POSTGRES_USER: telemetry - POSTGRES_PASSWORD: telemetry - POSTGRES_DB: telemetry - volumes: - - pgdata:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U telemetry"] - interval: 5s - timeout: 5s - retries: 5 - restart: unless-stopped - - caddy: - image: caddy:2-alpine - ports: - - "80:80" - - "443:443" - volumes: - - ./deployment/Caddyfile:/etc/caddy/Caddyfile:ro - - caddy_data:/data - - caddy_config:/config - depends_on: - - telemetry-server - restart: unless-stopped - -volumes: - pgdata: - caddy_data: - caddy_config: diff --git a/clp-telemetry-server/src/main.rs b/clp-telemetry-server/src/main.rs deleted file mode 100644 index 18286c0a24..0000000000 --- a/clp-telemetry-server/src/main.rs +++ /dev/null @@ -1,156 +0,0 @@ -use std::net::SocketAddr; -use std::sync::Arc; - -use axum::extract::State; -use axum::http::StatusCode; -use axum::routing::post; -use axum::{Json, Router}; -use serde::{Deserialize, Serialize}; -use sqlx::PgPool; -use tower_http::limit::RequestBodyLimitLayer; - -/// Maximum request body size (10 KB). -const MAX_BODY_SIZE: usize = 10 * 1024; - -#[derive(Debug, Deserialize, Serialize)] -struct TelemetryEvent { - schema_version: u32, - telemetry_id: String, - timestamp: String, - event_type: String, - #[serde(default)] - clp_version: String, - #[serde(default)] - deployment_method: String, - #[serde(default)] - os: String, - #[serde(default)] - os_version: String, - #[serde(default)] - arch: String, - #[serde(default)] - storage_engine: String, - #[serde(default)] - payload: Option, -} - -struct AppState { - db: PgPool, -} - -async fn ingest_event( - State(state): State>, - Json(event): Json, -) -> StatusCode { - // Validate required fields - if event.telemetry_id.is_empty() || event.event_type.is_empty() { - return StatusCode::BAD_REQUEST; - } - - // Validate telemetry_id is a plausible UUID (basic check) - if event.telemetry_id.len() > 64 { - return StatusCode::BAD_REQUEST; - } - - let payload_json = event - .payload - .as_ref() - .map(|p| serde_json::to_string(p).unwrap_or_default()); - - let result = sqlx::query( - r#" - INSERT INTO telemetry_events ( - schema_version, telemetry_id, event_timestamp, event_type, - clp_version, deployment_method, os, os_version, arch, - storage_engine, payload - ) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) - "#, - ) - .bind(event.schema_version as i32) - .bind(&event.telemetry_id) - .bind(&event.timestamp) - .bind(&event.event_type) - .bind(&event.clp_version) - .bind(&event.deployment_method) - .bind(&event.os) - .bind(&event.os_version) - .bind(&event.arch) - .bind(&event.storage_engine) - .bind(payload_json) - .execute(&state.db) - .await; - - match result { - Ok(_) => StatusCode::CREATED, - Err(e) => { - tracing::error!("Failed to insert telemetry event: {e}"); - StatusCode::INTERNAL_SERVER_ERROR - } - } -} - -async fn health() -> StatusCode { - StatusCode::OK -} - -#[tokio::main] -async fn main() -> Result<(), Box> { - tracing_subscriber::fmt() - .with_env_filter( - tracing_subscriber::EnvFilter::try_from_default_env() - .unwrap_or_else(|_| "info".into()), - ) - .init(); - - let database_url = - std::env::var("DATABASE_URL").expect("DATABASE_URL environment variable must be set"); - - let db = PgPool::connect(&database_url).await?; - - // Run migrations on startup - sqlx::query( - r#" - CREATE TABLE IF NOT EXISTS telemetry_events ( - id BIGSERIAL PRIMARY KEY, - schema_version INTEGER NOT NULL, - telemetry_id TEXT NOT NULL, - event_timestamp TEXT NOT NULL, - event_type TEXT NOT NULL, - clp_version TEXT NOT NULL DEFAULT '', - deployment_method TEXT NOT NULL DEFAULT '', - os TEXT NOT NULL DEFAULT '', - os_version TEXT NOT NULL DEFAULT '', - arch TEXT NOT NULL DEFAULT '', - storage_engine TEXT NOT NULL DEFAULT '', - payload TEXT, - received_at TIMESTAMPTZ NOT NULL DEFAULT NOW() - ); - - CREATE INDEX IF NOT EXISTS idx_telemetry_events_telemetry_id - ON telemetry_events (telemetry_id); - CREATE INDEX IF NOT EXISTS idx_telemetry_events_event_type - ON telemetry_events (event_type); - CREATE INDEX IF NOT EXISTS idx_telemetry_events_received_at - ON telemetry_events (received_at); - "#, - ) - .execute(&db) - .await?; - - let state = Arc::new(AppState { db }); - - let app = Router::new() - .route("/v1/events", post(ingest_event)) - .route("/health", axum::routing::get(health)) - .layer(RequestBodyLimitLayer::new(MAX_BODY_SIZE)) - .with_state(state); - - let addr: SocketAddr = "0.0.0.0:8080".parse()?; - tracing::info!("Telemetry server listening on {addr}"); - - let listener = tokio::net::TcpListener::bind(&addr).await?; - axum::serve(listener, app).await?; - - Ok(()) -} From 8e098019e9ff1424545a90acf484403561d38953 Mon Sep 17 00:00:00 2001 From: Nathan <30803215+Nathan903@users.noreply.github.com> Date: Fri, 6 Mar 2026 02:30:53 -0500 Subject: [PATCH 03/27] fix cargo test failure --- components/clp-rust-utils/src/database/mysql.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/clp-rust-utils/src/database/mysql.rs b/components/clp-rust-utils/src/database/mysql.rs index 1ea951184a..37137389b9 100644 --- a/components/clp-rust-utils/src/database/mysql.rs +++ b/components/clp-rust-utils/src/database/mysql.rs @@ -10,7 +10,7 @@ use crate::clp_config::package::{ /// /// # Examples /// -/// ```rust +/// ```rust,ignore /// impl_sqlx_type!(IngestedS3ObjectMetadataStatus => str); /// ``` #[macro_export] From f52fd36d20ad8251b380a14722ffbdf162ba595a Mon Sep 17 00:00:00 2001 From: Nathan <30803215+Nathan903@users.noreply.github.com> Date: Fri, 6 Mar 2026 03:17:42 -0500 Subject: [PATCH 04/27] chore: update Cargo.lock. --- Cargo.lock | 267 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 267 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index d0992fe331..95971e3474 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -45,6 +45,15 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.21" @@ -109,6 +118,7 @@ dependencies = [ "async-stream", "aws-sdk-s3", "axum", + "chrono", "clap", "clp-rust-utils", "futures", @@ -116,6 +126,7 @@ dependencies = [ "non-empty-string", "num_enum", "pin-project-lite", + "reqwest", "rmp-serde", "secrecy", "serde", @@ -127,6 +138,7 @@ dependencies = [ "tracing", "utoipa", "utoipa-axum", + "uuid", ] [[package]] @@ -840,6 +852,26 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "clap" version = "4.5.56" @@ -1892,6 +1924,7 @@ dependencies = [ "tokio", "tokio-rustls 0.26.4", "tower-service", + "webpki-roots", ] [[package]] @@ -1918,6 +1951,30 @@ dependencies = [ "tracing", ] +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "2.1.1" @@ -2056,6 +2113,16 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -2190,6 +2257,12 @@ dependencies = [ "hashbrown 0.16.1", ] +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "macro_magic" version = "0.5.1" @@ -2667,6 +2740,61 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls 0.23.36", + "socket2 0.6.2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls 0.23.36", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.6.2", + "tracing", + "windows-sys 0.60.2", +] + [[package]] name = "quote" version = "1.0.44" @@ -2800,6 +2928,44 @@ version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "futures-core", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.8.1", + "hyper-rustls 0.27.7", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls 0.23.36", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls 0.26.4", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + [[package]] name = "resolv-conf" version = "0.7.6" @@ -2870,6 +3036,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustc_version" version = "0.4.1" @@ -2935,6 +3107,7 @@ version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ + "web-time", "zeroize", ] @@ -3602,6 +3775,9 @@ name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] [[package]] name = "synstructure" @@ -3853,8 +4029,12 @@ checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ "bitflags", "bytes", + "futures-util", "http 1.4.0", + "http-body 1.0.1", + "iri-string", "pin-project-lite", + "tower", "tower-layer", "tower-service", ] @@ -4187,6 +4367,20 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.108" @@ -4219,6 +4413,26 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "web-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "webpki-roots" version = "1.0.5" @@ -4244,12 +4458,65 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.48.0" From ece60a038e63b5bcb5a4c2f72b4f1303e9e066e7 Mon Sep 17 00:00:00 2001 From: Nathan <30803215+Nathan903@users.noreply.github.com> Date: Fri, 6 Mar 2026 03:36:07 -0500 Subject: [PATCH 05/27] refactor: streamline imports in telemetry module; update user docs to include telemetry reference --- components/api-server/src/telemetry.rs | 6 ++---- docs/src/user-docs/index.md | 8 ++++++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/components/api-server/src/telemetry.rs b/components/api-server/src/telemetry.rs index 8d3a2bfd73..66cb1c11d4 100644 --- a/components/api-server/src/telemetry.rs +++ b/components/api-server/src/telemetry.rs @@ -1,10 +1,8 @@ -use std::env; -use std::time::Duration; +use std::{env, time::Duration}; use chrono::Utc; -use serde::Serialize; - use clp_rust_utils::clp_config::package::config::Config; +use serde::Serialize; const TELEMETRY_ENDPOINT: &str = "https://telemetry.yscope.io/v1/events"; const TELEMETRY_SEND_INTERVAL: Duration = Duration::from_secs(24 * 60 * 60); // 24 hours diff --git a/docs/src/user-docs/index.md b/docs/src/user-docs/index.md index 33c64d9f93..8621c65398 100644 --- a/docs/src/user-docs/index.md +++ b/docs/src/user-docs/index.md @@ -214,6 +214,13 @@ Schema file syntax ^^^ Syntax reference for clp's schema file for parsing unstructured text logs. ::: + +:::{grid-item-card} +:link: reference-telemetry +Telemetry +^^^ +Details on CLP's anonymous usage telemetry and how to disable it. +::: :::: :::{toctree} @@ -283,4 +290,5 @@ reference-json-search-syntax reference-text-search-syntax reference-sbin-scripts/index reference-unstructured-schema-file +reference-telemetry ::: From 2d36ab0c42071b6ee40d3afccd6532a20ec02633 Mon Sep 17 00:00:00 2001 From: Nathan <30803215+Nathan903@users.noreply.github.com> Date: Fri, 6 Mar 2026 03:37:11 -0500 Subject: [PATCH 06/27] chore: update version to 0.2.0-dev.2 in Helm chart --- tools/deployment/package-helm/Chart.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/deployment/package-helm/Chart.yaml b/tools/deployment/package-helm/Chart.yaml index 22eb0a3a98..93c3ff99be 100644 --- a/tools/deployment/package-helm/Chart.yaml +++ b/tools/deployment/package-helm/Chart.yaml @@ -1,6 +1,6 @@ apiVersion: "v2" name: "clp" -version: "0.2.0-dev.1" +version: "0.2.0-dev.2" description: "A Helm chart for CLP's (Compressed Log Processor) package deployment" type: "application" appVersion: "0.9.1-dev" From 328432d523c7cfce523409b1f8f62e1abde2142c Mon Sep 17 00:00:00 2001 From: Nathan <30803215+Nathan903@users.noreply.github.com> Date: Fri, 6 Mar 2026 03:51:34 -0500 Subject: [PATCH 07/27] refactor: rust linter --- components/api-server/src/telemetry.rs | 25 +++++++++---------- .../src/clp_config/package/config.rs | 8 +----- 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/components/api-server/src/telemetry.rs b/components/api-server/src/telemetry.rs index 66cb1c11d4..d228df68ad 100644 --- a/components/api-server/src/telemetry.rs +++ b/components/api-server/src/telemetry.rs @@ -5,7 +5,7 @@ use clp_rust_utils::clp_config::package::config::Config; use serde::Serialize; const TELEMETRY_ENDPOINT: &str = "https://telemetry.yscope.io/v1/events"; -const TELEMETRY_SEND_INTERVAL: Duration = Duration::from_secs(24 * 60 * 60); // 24 hours +const TELEMETRY_SEND_INTERVAL: Duration = Duration::from_hours(24); // 24 hours const TELEMETRY_HTTP_TIMEOUT: Duration = Duration::from_secs(5); /// Schema version for the telemetry payload. @@ -35,23 +35,23 @@ fn is_telemetry_disabled(config: &Config) -> bool { } // Check CLP_DISABLE_TELEMETRY env var - if let Ok(val) = env::var("CLP_DISABLE_TELEMETRY") { - if val.eq_ignore_ascii_case("true") || val == "1" { - return true; - } + if let Ok(val) = env::var("CLP_DISABLE_TELEMETRY") + && (val.eq_ignore_ascii_case("true") || val == "1") + { + return true; } // Check DO_NOT_TRACK env var (https://consoledonottrack.com/) - if let Ok(val) = env::var("DO_NOT_TRACK") { - if val == "1" { - return true; - } + if let Ok(val) = env::var("DO_NOT_TRACK") + && val == "1" + { + return true; } false } -fn get_storage_engine_str(config: &Config) -> &'static str { +const fn get_storage_engine_str(config: &Config) -> &'static str { match config.package.storage_engine { clp_rust_utils::clp_config::package::config::StorageEngine::Clp => "clp", clp_rust_utils::clp_config::package::config::StorageEngine::ClpS => "clp-s", @@ -83,9 +83,8 @@ fn build_event(event_type: &str, config: &Config) -> TelemetryEvent { } async fn send_event(client: &reqwest::Client, event: &TelemetryEvent) { - let is_debug = env::var("CLP_TELEMETRY_DEBUG") - .map(|v| v.eq_ignore_ascii_case("true") || v == "1") - .unwrap_or(false); + let is_debug = + env::var("CLP_TELEMETRY_DEBUG").is_ok_and(|v| v.eq_ignore_ascii_case("true") || v == "1"); if is_debug { match serde_json::to_string_pretty(event) { diff --git a/components/clp-rust-utils/src/clp_config/package/config.rs b/components/clp-rust-utils/src/clp_config/package/config.rs index fa5bba60c3..bef7402073 100644 --- a/components/clp-rust-utils/src/clp_config/package/config.rs +++ b/components/clp-rust-utils/src/clp_config/package/config.rs @@ -319,18 +319,12 @@ pub enum LogsInput { } /// Mirror of `clp_py_utils.clp_config.Telemetry`. -#[derive(Clone, Debug, Deserialize, Eq, PartialEq)] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)] #[serde(default)] pub struct Telemetry { pub disable: bool, } -impl Default for Telemetry { - fn default() -> Self { - Self { disable: false } - } -} - #[cfg(test)] mod tests { use super::LogsInput; From 88a6565b3fd7ca1bff02f2ba19a51b351da453c1 Mon Sep 17 00:00:00 2001 From: Nathan <30803215+Nathan903@users.noreply.github.com> Date: Fri, 6 Mar 2026 12:01:18 -0500 Subject: [PATCH 08/27] refactor: py linter --- .../clp-package-utils/clp_package_utils/controller.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/components/clp-package-utils/clp_package_utils/controller.py b/components/clp-package-utils/clp_package_utils/controller.py index 64a149a344..493c616433 100644 --- a/components/clp-package-utils/clp_package_utils/controller.py +++ b/components/clp-package-utils/clp_package_utils/controller.py @@ -649,12 +649,14 @@ def _set_up_env_for_api_server(self) -> EnvVarsDict: instance_id_file = self._clp_config.logs_directory / "instance-id" resolved_id_file = resolve_host_path_in_container(instance_id_file) if resolved_id_file.exists(): - with open(resolved_id_file, "r") as f: + with resolved_id_file.open("r") as f: env_vars["CLP_INSTANCE_ID"] = f.readline().strip() - version_file = resolve_host_path_in_container(self._clp_config._version_file_path) + version_file = resolve_host_path_in_container( + self._clp_config.logs_directory.parent / "VERSION" + ) if version_file.exists(): - with open(version_file, "r") as f: + with version_file.open("r") as f: env_vars["CLP_VERSION"] = f.read().strip() env_vars["CLP_DEPLOYMENT_METHOD"] = "docker-compose" From 03dca64821049e3f866c9d674681633491ae865e Mon Sep 17 00:00:00 2001 From: Nathan <30803215+Nathan903@users.noreply.github.com> Date: Fri, 6 Mar 2026 12:49:59 -0500 Subject: [PATCH 09/27] feat: Implement graceful shutdown for the telemetry background task using `CancellationToken`. --- Cargo.lock | 1 + components/api-server/Cargo.toml | 1 + components/api-server/src/bin/api_server.rs | 10 +++++++- components/api-server/src/telemetry.rs | 28 +++++++++++++++------ 4 files changed, 32 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 95971e3474..d31e88182d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -134,6 +134,7 @@ dependencies = [ "sqlx", "thiserror", "tokio", + "tokio-util", "tower-http", "tracing", "utoipa", diff --git a/components/api-server/Cargo.toml b/components/api-server/Cargo.toml index 2a2a2010b4..def00efd2e 100644 --- a/components/api-server/Cargo.toml +++ b/components/api-server/Cargo.toml @@ -34,6 +34,7 @@ serde_json = "1.0.149" sqlx = { version = "0.8.6", features = ["runtime-tokio", "mysql"] } thiserror = "2.0.18" tokio = { version = "1.49.0", features = ["full"] } +tokio-util = "0.7" tower-http = { version = "0.6.8", features = ["cors"] } tracing = "0.1.44" uuid = { version = "1", features = ["v4"] } diff --git a/components/api-server/src/bin/api_server.rs b/components/api-server/src/bin/api_server.rs index cac7936e96..e98fcf38a7 100644 --- a/components/api-server/src/bin/api_server.rs +++ b/components/api-server/src/bin/api_server.rs @@ -71,7 +71,11 @@ async fn main() -> anyhow::Result<()> { .context("Cannot connect to CLP")?; // Spawn telemetry background task (non-blocking, failures are silent) - tokio::spawn(api_server::telemetry::run_telemetry_loop(config)); + let telemetry_cancel = tokio_util::sync::CancellationToken::new(); + tokio::spawn(api_server::telemetry::run_telemetry_loop( + config, + telemetry_cancel.clone(), + )); let router = api_server::routes::from_client(client)?; @@ -79,5 +83,9 @@ async fn main() -> anyhow::Result<()> { axum::serve(listener, router) .with_graceful_shutdown(shutdown_signal()) .await?; + + // Signal the telemetry loop to stop + telemetry_cancel.cancel(); + Ok(()) } diff --git a/components/api-server/src/telemetry.rs b/components/api-server/src/telemetry.rs index d228df68ad..88b9ce9791 100644 --- a/components/api-server/src/telemetry.rs +++ b/components/api-server/src/telemetry.rs @@ -1,5 +1,7 @@ use std::{env, time::Duration}; +use tokio_util::sync::CancellationToken; + use chrono::Utc; use clp_rust_utils::clp_config::package::config::Config; use serde::Serialize; @@ -107,11 +109,17 @@ async fn send_event(client: &reqwest::Client, event: &TelemetryEvent) { /// Runs the telemetry background loop. Sends a `deployment_start` event on startup /// and a `heartbeat` event every 24 hours. All failures are silently ignored. /// +/// The loop respects the given [`CancellationToken`]: when the token is cancelled +/// the function returns promptly, allowing a clean shutdown. +/// /// This function is designed to be spawned as a background tokio task: /// ```ignore -/// tokio::spawn(telemetry::run_telemetry_loop(config)); +/// let cancel = CancellationToken::new(); +/// tokio::spawn(telemetry::run_telemetry_loop(config, cancel.clone())); +/// // later, to stop: +/// cancel.cancel(); /// ``` -pub async fn run_telemetry_loop(config: Config) { +pub async fn run_telemetry_loop(config: Config, cancel: CancellationToken) { if is_telemetry_disabled(&config) { tracing::info!("Anonymous telemetry is disabled."); return; @@ -134,11 +142,17 @@ pub async fn run_telemetry_loop(config: Config) { let start_event = build_event("deployment_start", &config); send_event(&client, &start_event).await; - // Periodic heartbeat + // Periodic heartbeat — exit when cancellation is requested loop { - tokio::time::sleep(TELEMETRY_SEND_INTERVAL).await; - - let heartbeat_event = build_event("heartbeat", &config); - send_event(&client, &heartbeat_event).await; + tokio::select! { + () = cancel.cancelled() => { + tracing::info!("Telemetry loop cancelled, shutting down."); + break; + } + () = tokio::time::sleep(TELEMETRY_SEND_INTERVAL) => { + let heartbeat_event = build_event("heartbeat", &config); + send_event(&client, &heartbeat_event).await; + } + } } } From b17a4ff04ae894a4a29de7cc4b9579c3def195e8 Mon Sep 17 00:00:00 2001 From: Nathan <30803215+Nathan903@users.noreply.github.com> Date: Fri, 6 Mar 2026 12:51:30 -0500 Subject: [PATCH 10/27] Revert "feat: Implement graceful shutdown for the telemetry background task using `CancellationToken`." This reverts commit 03dca64821049e3f866c9d674681633491ae865e. --- Cargo.lock | 1 - components/api-server/Cargo.toml | 1 - components/api-server/src/bin/api_server.rs | 10 +------- components/api-server/src/telemetry.rs | 28 ++++++--------------- 4 files changed, 8 insertions(+), 32 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d31e88182d..95971e3474 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -134,7 +134,6 @@ dependencies = [ "sqlx", "thiserror", "tokio", - "tokio-util", "tower-http", "tracing", "utoipa", diff --git a/components/api-server/Cargo.toml b/components/api-server/Cargo.toml index def00efd2e..2a2a2010b4 100644 --- a/components/api-server/Cargo.toml +++ b/components/api-server/Cargo.toml @@ -34,7 +34,6 @@ serde_json = "1.0.149" sqlx = { version = "0.8.6", features = ["runtime-tokio", "mysql"] } thiserror = "2.0.18" tokio = { version = "1.49.0", features = ["full"] } -tokio-util = "0.7" tower-http = { version = "0.6.8", features = ["cors"] } tracing = "0.1.44" uuid = { version = "1", features = ["v4"] } diff --git a/components/api-server/src/bin/api_server.rs b/components/api-server/src/bin/api_server.rs index e98fcf38a7..cac7936e96 100644 --- a/components/api-server/src/bin/api_server.rs +++ b/components/api-server/src/bin/api_server.rs @@ -71,11 +71,7 @@ async fn main() -> anyhow::Result<()> { .context("Cannot connect to CLP")?; // Spawn telemetry background task (non-blocking, failures are silent) - let telemetry_cancel = tokio_util::sync::CancellationToken::new(); - tokio::spawn(api_server::telemetry::run_telemetry_loop( - config, - telemetry_cancel.clone(), - )); + tokio::spawn(api_server::telemetry::run_telemetry_loop(config)); let router = api_server::routes::from_client(client)?; @@ -83,9 +79,5 @@ async fn main() -> anyhow::Result<()> { axum::serve(listener, router) .with_graceful_shutdown(shutdown_signal()) .await?; - - // Signal the telemetry loop to stop - telemetry_cancel.cancel(); - Ok(()) } diff --git a/components/api-server/src/telemetry.rs b/components/api-server/src/telemetry.rs index 88b9ce9791..d228df68ad 100644 --- a/components/api-server/src/telemetry.rs +++ b/components/api-server/src/telemetry.rs @@ -1,7 +1,5 @@ use std::{env, time::Duration}; -use tokio_util::sync::CancellationToken; - use chrono::Utc; use clp_rust_utils::clp_config::package::config::Config; use serde::Serialize; @@ -109,17 +107,11 @@ async fn send_event(client: &reqwest::Client, event: &TelemetryEvent) { /// Runs the telemetry background loop. Sends a `deployment_start` event on startup /// and a `heartbeat` event every 24 hours. All failures are silently ignored. /// -/// The loop respects the given [`CancellationToken`]: when the token is cancelled -/// the function returns promptly, allowing a clean shutdown. -/// /// This function is designed to be spawned as a background tokio task: /// ```ignore -/// let cancel = CancellationToken::new(); -/// tokio::spawn(telemetry::run_telemetry_loop(config, cancel.clone())); -/// // later, to stop: -/// cancel.cancel(); +/// tokio::spawn(telemetry::run_telemetry_loop(config)); /// ``` -pub async fn run_telemetry_loop(config: Config, cancel: CancellationToken) { +pub async fn run_telemetry_loop(config: Config) { if is_telemetry_disabled(&config) { tracing::info!("Anonymous telemetry is disabled."); return; @@ -142,17 +134,11 @@ pub async fn run_telemetry_loop(config: Config, cancel: CancellationToken) { let start_event = build_event("deployment_start", &config); send_event(&client, &start_event).await; - // Periodic heartbeat — exit when cancellation is requested + // Periodic heartbeat loop { - tokio::select! { - () = cancel.cancelled() => { - tracing::info!("Telemetry loop cancelled, shutting down."); - break; - } - () = tokio::time::sleep(TELEMETRY_SEND_INTERVAL) => { - let heartbeat_event = build_event("heartbeat", &config); - send_event(&client, &heartbeat_event).await; - } - } + tokio::time::sleep(TELEMETRY_SEND_INTERVAL).await; + + let heartbeat_event = build_event("heartbeat", &config); + send_event(&client, &heartbeat_event).await; } } From 807bcb6ce37ecae897d76c48201c22691f9b3fbb Mon Sep 17 00:00:00 2001 From: Nathan <30803215+Nathan903@users.noreply.github.com> Date: Fri, 6 Mar 2026 12:53:58 -0500 Subject: [PATCH 11/27] Reorder router initialization to occur before telemetry setup. --- components/api-server/src/bin/api_server.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/api-server/src/bin/api_server.rs b/components/api-server/src/bin/api_server.rs index cac7936e96..27119f9be9 100644 --- a/components/api-server/src/bin/api_server.rs +++ b/components/api-server/src/bin/api_server.rs @@ -70,11 +70,11 @@ async fn main() -> anyhow::Result<()> { .await .context("Cannot connect to CLP")?; + let router = api_server::routes::from_client(client)?; + // Spawn telemetry background task (non-blocking, failures are silent) tokio::spawn(api_server::telemetry::run_telemetry_loop(config)); - let router = api_server::routes::from_client(client)?; - tracing::info!("Server started at {addr}"); axum::serve(listener, router) .with_graceful_shutdown(shutdown_signal()) From 72b77d4e29af7fd0ea1470f42ebd6e96367147a9 Mon Sep 17 00:00:00 2001 From: Nathan <30803215+Nathan903@users.noreply.github.com> Date: Fri, 6 Mar 2026 12:55:43 -0500 Subject: [PATCH 12/27] fix: Resolve CLP_VERSION file path using `_clp_home` instead of `logs_directory.parent`. --- components/clp-package-utils/clp_package_utils/controller.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/components/clp-package-utils/clp_package_utils/controller.py b/components/clp-package-utils/clp_package_utils/controller.py index 493c616433..1e16862d3f 100644 --- a/components/clp-package-utils/clp_package_utils/controller.py +++ b/components/clp-package-utils/clp_package_utils/controller.py @@ -652,9 +652,7 @@ def _set_up_env_for_api_server(self) -> EnvVarsDict: with resolved_id_file.open("r") as f: env_vars["CLP_INSTANCE_ID"] = f.readline().strip() - version_file = resolve_host_path_in_container( - self._clp_config.logs_directory.parent / "VERSION" - ) + version_file = resolve_host_path_in_container(self._clp_home / "VERSION") if version_file.exists(): with version_file.open("r") as f: env_vars["CLP_VERSION"] = f.read().strip() From ffa6dda42f9a535d1bc6ef8718b1ecff83436fcc Mon Sep 17 00:00:00 2001 From: Nathan <30803215+Nathan903@users.noreply.github.com> Date: Fri, 6 Mar 2026 12:57:47 -0500 Subject: [PATCH 13/27] fix: make `CLP_DISABLE_TELEMETRY` environment variable check case-insensitive --- components/package-template/src/sbin/start-clp.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/components/package-template/src/sbin/start-clp.sh b/components/package-template/src/sbin/start-clp.sh index 7434e889d6..0fa8a0d4e8 100755 --- a/components/package-template/src/sbin/start-clp.sh +++ b/components/package-template/src/sbin/start-clp.sh @@ -24,7 +24,9 @@ telemetry_prompt_needed=false clp_config_path="${CLP_HOME}/etc/clp-config.yaml" # Check env vars first -if [[ "${CLP_DISABLE_TELEMETRY:-}" == "true" ]] || [[ "${CLP_DISABLE_TELEMETRY:-}" == "1" ]]; then +clp_disable_telemetry_lower="${CLP_DISABLE_TELEMETRY:-}" +clp_disable_telemetry_lower="${clp_disable_telemetry_lower,,}" +if [[ "$clp_disable_telemetry_lower" == "true" ]] || [[ "$clp_disable_telemetry_lower" == "1" ]]; then telemetry_prompt_needed=false elif [[ "${DO_NOT_TRACK:-}" == "1" ]]; then telemetry_prompt_needed=false From 5da85ccae57dbd9fd07ebc188de37932d0dc99b4 Mon Sep 17 00:00:00 2001 From: Nathan <30803215+Nathan903@users.noreply.github.com> Date: Fri, 6 Mar 2026 12:59:56 -0500 Subject: [PATCH 14/27] fix: replace existing telemetry configuration block or append a new one when disabling telemetry. --- .../package-template/src/sbin/start-clp.sh | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/components/package-template/src/sbin/start-clp.sh b/components/package-template/src/sbin/start-clp.sh index 0fa8a0d4e8..9ce1ec76f0 100755 --- a/components/package-template/src/sbin/start-clp.sh +++ b/components/package-template/src/sbin/start-clp.sh @@ -62,9 +62,19 @@ if [[ "$telemetry_prompt_needed" == "true" ]]; then if [[ "$telemetry_response" =~ ^[Nn]$ ]]; then # User opted out — persist to config if [[ -f "$clp_config_path" ]]; then - echo "" >> "$clp_config_path" - echo "telemetry:" >> "$clp_config_path" - echo " disable: true" >> "$clp_config_path" + if grep -q "^telemetry:" "$clp_config_path" 2>/dev/null; then + # Replace existing telemetry block (key + indented lines) + sed -i '/^telemetry:/,/^[^[:space:]]/{/^telemetry:/!{/^[^[:space:]]/!d};}' \ + "$clp_config_path" + sed -i 's/^telemetry:.*/telemetry:\n disable: true/' "$clp_config_path" + else + # Append telemetry block with grouped redirect + { + echo "" + echo "telemetry:" + echo " disable: true" + } >> "$clp_config_path" + fi else printf "telemetry:\n disable: true\n" > "$clp_config_path" fi From e997a5ea4927f574bf83dcd4536b422616a859c8 Mon Sep 17 00:00:00 2001 From: Nathan <30803215+Nathan903@users.noreply.github.com> Date: Fri, 6 Mar 2026 13:00:43 -0500 Subject: [PATCH 15/27] refactor: separate `CLP_HOST_ARCH` export from its assignment. --- components/package-template/src/sbin/start-clp.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/components/package-template/src/sbin/start-clp.sh b/components/package-template/src/sbin/start-clp.sh index 9ce1ec76f0..8cf881be32 100755 --- a/components/package-template/src/sbin/start-clp.sh +++ b/components/package-template/src/sbin/start-clp.sh @@ -92,7 +92,8 @@ else CLP_HOST_OS_VERSION="unknown" fi export CLP_HOST_OS_VERSION -export CLP_HOST_ARCH="$(uname -m)" +export CLP_HOST_ARCH +CLP_HOST_ARCH="$(uname -m)" docker compose -f "$CLP_HOME/docker-compose.runtime.yaml" \ run --rm "${CLP_COMPOSE_RUN_EXTRA_FLAGS[@]}" clp-runtime \ From ca531c5c07a3efd321822cd66b4150200398a164 Mon Sep 17 00:00:00 2001 From: Nathan <30803215+Nathan903@users.noreply.github.com> Date: Fri, 6 Mar 2026 13:02:44 -0500 Subject: [PATCH 16/27] docs: add GitHub links to telemetry client, consent prompt, and server components. --- docs/src/user-docs/reference-telemetry.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/src/user-docs/reference-telemetry.md b/docs/src/user-docs/reference-telemetry.md index 1570d5d574..713f0da620 100644 --- a/docs/src/user-docs/reference-telemetry.md +++ b/docs/src/user-docs/reference-telemetry.md @@ -97,7 +97,10 @@ The JSON payload will be logged to the API server log file instead of being tran The telemetry implementation is fully open source: -- **Client**: `components/api-server/src/telemetry.rs` -- **Consent prompt**: `components/package-template/src/sbin/start-clp.sh` +- **Client**: + [components/api-server/src/telemetry.rs](https://github.com/y-scope/clp/blob/DOCS_VAR_CLP_GIT_REF/components/api-server/src/telemetry.rs) +- **Consent prompt**: + [components/package-template/src/sbin/start-clp.sh](https://github.com/y-scope/clp/blob/DOCS_VAR_CLP_GIT_REF/components/package-template/src/sbin/start-clp.sh) - **Configuration**: `telemetry.disable` in `clp-config.yaml` -- **Server**: `clp-telemetry-server/` +- **Server**: + [clp-telemetry-server](https://github.com/y-scope/clp-telemetry-server) From 07d9959f4f1dd8959c5f39fe46e4cce535c75763 Mon Sep 17 00:00:00 2001 From: Nathan <30803215+Nathan903@users.noreply.github.com> Date: Fri, 6 Mar 2026 13:04:40 -0500 Subject: [PATCH 17/27] fix: improve telemetry config detection by anchoring grep pattern to line start. --- components/package-template/src/sbin/start-clp.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/package-template/src/sbin/start-clp.sh b/components/package-template/src/sbin/start-clp.sh index 8cf881be32..f301922fc2 100755 --- a/components/package-template/src/sbin/start-clp.sh +++ b/components/package-template/src/sbin/start-clp.sh @@ -31,7 +31,7 @@ if [[ "$clp_disable_telemetry_lower" == "true" ]] || [[ "$clp_disable_telemetry_ elif [[ "${DO_NOT_TRACK:-}" == "1" ]]; then telemetry_prompt_needed=false # Check if telemetry is already configured in the config file -elif [[ -f "$clp_config_path" ]] && grep -q "telemetry:" "$clp_config_path" 2>/dev/null; then +elif [[ -f "$clp_config_path" ]] && grep -q -E '^telemetry:' "$clp_config_path" 2>/dev/null; then telemetry_prompt_needed=false # Check if instance-id exists (not first run) elif [[ -f "${CLP_HOME}/var/log/instance-id" ]]; then From cfc2f8a1f7d9f43272ebc8ef07be1c603d632502 Mon Sep 17 00:00:00 2001 From: Nathan <30803215+Nathan903@users.noreply.github.com> Date: Fri, 6 Mar 2026 13:06:19 -0500 Subject: [PATCH 18/27] Update Docker Compose project name to use the full instance ID. --- components/clp-package-utils/clp_package_utils/controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/clp-package-utils/clp_package_utils/controller.py b/components/clp-package-utils/clp_package_utils/controller.py index 1e16862d3f..f40fbe1055 100644 --- a/components/clp-package-utils/clp_package_utils/controller.py +++ b/components/clp-package-utils/clp_package_utils/controller.py @@ -990,7 +990,7 @@ def __init__( self, clp_config: ClpConfig, instance_id: str, restart_policy: str = "on-failure:3" ) -> None: """Initializes the DockerComposeController.""" - self._project_name = f"clp-package-{instance_id[-4:]}" + self._project_name = f"clp-package-{instance_id}" self._restart_policy = restart_policy super().__init__(clp_config) From 2e7cc5ab77aa8cf81c8fe8a88f03b16e86a64cfb Mon Sep 17 00:00:00 2001 From: Nathan <30803215+Nathan903@users.noreply.github.com> Date: Fri, 6 Mar 2026 13:08:12 -0500 Subject: [PATCH 19/27] build: Add `rust-version = "1.85"` to `Cargo.toml` files for api-server, clp-rust-utils, and log-ingestor. --- components/api-server/Cargo.toml | 1 + components/clp-rust-utils/Cargo.toml | 1 + components/log-ingestor/Cargo.toml | 1 + 3 files changed, 3 insertions(+) diff --git a/components/api-server/Cargo.toml b/components/api-server/Cargo.toml index 2a2a2010b4..0eef219f01 100644 --- a/components/api-server/Cargo.toml +++ b/components/api-server/Cargo.toml @@ -2,6 +2,7 @@ name = "api-server" version = "0.1.0" edition = "2024" +rust-version = "1.85" [lib] name = "api_server" diff --git a/components/clp-rust-utils/Cargo.toml b/components/clp-rust-utils/Cargo.toml index b4d56ba353..62ffabb632 100644 --- a/components/clp-rust-utils/Cargo.toml +++ b/components/clp-rust-utils/Cargo.toml @@ -2,6 +2,7 @@ name = "clp-rust-utils" version = "0.1.0" edition = "2024" +rust-version = "1.85" [dependencies] aws-config = "1.8.12" diff --git a/components/log-ingestor/Cargo.toml b/components/log-ingestor/Cargo.toml index e1489b5f23..fd20dbb447 100644 --- a/components/log-ingestor/Cargo.toml +++ b/components/log-ingestor/Cargo.toml @@ -2,6 +2,7 @@ name = "log-ingestor" version = "0.1.0" edition = "2024" +rust-version = "1.85" [lib] name = "log_ingestor" From 2a141fa9f44f06a24f096ca0cd59a8c67d15a3d9 Mon Sep 17 00:00:00 2001 From: Nathan <30803215+Nathan903@users.noreply.github.com> Date: Fri, 6 Mar 2026 13:10:43 -0500 Subject: [PATCH 20/27] refactor: Strip whitespace from read instance IDs and generate 4-character hex IDs for new instances. --- components/clp-package-utils/clp_package_utils/controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/clp-package-utils/clp_package_utils/controller.py b/components/clp-package-utils/clp_package_utils/controller.py index f40fbe1055..f74b65a6bd 100644 --- a/components/clp-package-utils/clp_package_utils/controller.py +++ b/components/clp-package-utils/clp_package_utils/controller.py @@ -1179,9 +1179,9 @@ def get_or_create_instance_id(clp_config: ClpConfig) -> str: if resolved_instance_id_file_path.exists(): with open(resolved_instance_id_file_path, "r") as f: - instance_id = f.readline() + instance_id = f.readline().strip() else: - instance_id = str(uuid.uuid4()) + instance_id = uuid.uuid4().hex[-4:] with open(resolved_instance_id_file_path, "w") as f: f.write(instance_id) From 0ac933572a42639c1f68ecefd913e88c411b599c Mon Sep 17 00:00:00 2001 From: Nathan <30803215+Nathan903@users.noreply.github.com> Date: Fri, 6 Mar 2026 15:46:51 -0500 Subject: [PATCH 21/27] fix rust lint --- components/api-server/src/telemetry.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/api-server/src/telemetry.rs b/components/api-server/src/telemetry.rs index d228df68ad..9dbac1b400 100644 --- a/components/api-server/src/telemetry.rs +++ b/components/api-server/src/telemetry.rs @@ -5,7 +5,7 @@ use clp_rust_utils::clp_config::package::config::Config; use serde::Serialize; const TELEMETRY_ENDPOINT: &str = "https://telemetry.yscope.io/v1/events"; -const TELEMETRY_SEND_INTERVAL: Duration = Duration::from_hours(24); // 24 hours +const TELEMETRY_SEND_INTERVAL: Duration = Duration::from_secs(24 * 60 * 60); // 24 hours const TELEMETRY_HTTP_TIMEOUT: Duration = Duration::from_secs(5); /// Schema version for the telemetry payload. From fb32994e5ce414e376b75a05cb18a66b6a9d09e2 Mon Sep 17 00:00:00 2001 From: Nathan <30803215+Nathan903@users.noreply.github.com> Date: Fri, 6 Mar 2026 15:49:06 -0500 Subject: [PATCH 22/27] feat: Add DO_NOT_TRACK environment variable to telemetry configuration --- components/clp-package-utils/clp_package_utils/controller.py | 1 + 1 file changed, 1 insertion(+) diff --git a/components/clp-package-utils/clp_package_utils/controller.py b/components/clp-package-utils/clp_package_utils/controller.py index f74b65a6bd..664da87ab2 100644 --- a/components/clp-package-utils/clp_package_utils/controller.py +++ b/components/clp-package-utils/clp_package_utils/controller.py @@ -666,6 +666,7 @@ def _set_up_env_for_api_server(self) -> EnvVarsDict: "CLP_HOST_ARCH", "CLP_DISABLE_TELEMETRY", "CLP_TELEMETRY_DEBUG", + "DO_NOT_TRACK", ): val = os.environ.get(var) if val is not None: From 6e46a65e15770b0fc3cd1fa3cccda4fe97fde657 Mon Sep 17 00:00:00 2001 From: Nathan <30803215+Nathan903@users.noreply.github.com> Date: Fri, 6 Mar 2026 15:50:33 -0500 Subject: [PATCH 23/27] fix: Handle empty input for telemetry consent prompt --- components/package-template/src/sbin/start-clp.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/components/package-template/src/sbin/start-clp.sh b/components/package-template/src/sbin/start-clp.sh index f301922fc2..7fed8330e6 100755 --- a/components/package-template/src/sbin/start-clp.sh +++ b/components/package-template/src/sbin/start-clp.sh @@ -56,7 +56,8 @@ if [[ "$telemetry_prompt_needed" == "true" ]]; then echo "You can disable telemetry at any time by setting CLP_DISABLE_TELEMETRY=true" echo "or by blocking https://telemetry.yscope.io at the network level." echo "" - read -r -p "Enable anonymous telemetry to help improve CLP? [Y/n] " telemetry_response + read -r -p "Enable anonymous telemetry to help improve CLP? [Y/n] " telemetry_response \ + || telemetry_response="" echo "================================================================================" if [[ "$telemetry_response" =~ ^[Nn]$ ]]; then From 0d1339bab504928e2a961a20271504d8077cdb6a Mon Sep 17 00:00:00 2001 From: Nathan <30803215+Nathan903@users.noreply.github.com> Date: Fri, 6 Mar 2026 15:59:58 -0500 Subject: [PATCH 24/27] feat: Enhance telemetry consent logic to support configurable logs directory --- .../package-template/src/sbin/start-clp.sh | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/components/package-template/src/sbin/start-clp.sh b/components/package-template/src/sbin/start-clp.sh index 7fed8330e6..cd81228de6 100755 --- a/components/package-template/src/sbin/start-clp.sh +++ b/components/package-template/src/sbin/start-clp.sh @@ -34,10 +34,30 @@ elif [[ "${DO_NOT_TRACK:-}" == "1" ]]; then elif [[ -f "$clp_config_path" ]] && grep -q -E '^telemetry:' "$clp_config_path" 2>/dev/null; then telemetry_prompt_needed=false # Check if instance-id exists (not first run) -elif [[ -f "${CLP_HOME}/var/log/instance-id" ]]; then - telemetry_prompt_needed=false else - telemetry_prompt_needed=true + # Determine the logs directory: read from config if set, otherwise use the default. + clp_logs_dir="${CLP_HOME}/var/log" + if [[ -f "$clp_config_path" ]]; then + configured_logs_dir=$( + grep -E '^logs_directory:' "$clp_config_path" 2>/dev/null \ + | sed 's/^logs_directory:[[:space:]]*//' \ + | sed 's/[[:space:]]*$//' \ + | sed "s/^['\"]//; s/['\"]$//" + ) + if [[ -n "$configured_logs_dir" ]]; then + if [[ "$configured_logs_dir" == /* ]]; then + clp_logs_dir="$configured_logs_dir" + else + clp_logs_dir="${CLP_HOME}/${configured_logs_dir}" + fi + fi + fi + + if [[ -f "${clp_logs_dir}/instance-id" ]]; then + telemetry_prompt_needed=false + else + telemetry_prompt_needed=true + fi fi if [[ "$telemetry_prompt_needed" == "true" ]]; then From 7c2a4d64bbfff701e9b117bf0dba1915130d7eb1 Mon Sep 17 00:00:00 2001 From: Nathan <30803215+Nathan903@users.noreply.github.com> Date: Fri, 6 Mar 2026 16:01:50 -0500 Subject: [PATCH 25/27] fix: Update instance ID generation to use full UUID instead of truncated version --- components/clp-package-utils/clp_package_utils/controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/clp-package-utils/clp_package_utils/controller.py b/components/clp-package-utils/clp_package_utils/controller.py index 664da87ab2..593b2f25c9 100644 --- a/components/clp-package-utils/clp_package_utils/controller.py +++ b/components/clp-package-utils/clp_package_utils/controller.py @@ -1182,7 +1182,7 @@ def get_or_create_instance_id(clp_config: ClpConfig) -> str: with open(resolved_instance_id_file_path, "r") as f: instance_id = f.readline().strip() else: - instance_id = uuid.uuid4().hex[-4:] + instance_id = uuid.uuid4().hex with open(resolved_instance_id_file_path, "w") as f: f.write(instance_id) From 0404d61f67f05a22e98c3c284dd8d38b83fdf01d Mon Sep 17 00:00:00 2001 From: Nathan <30803215+Nathan903@users.noreply.github.com> Date: Fri, 6 Mar 2026 16:11:02 -0500 Subject: [PATCH 26/27] fix: Update instance ID validation to accept full UUIDs and 4-character hex strings --- integration-tests/tests/utils/config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/integration-tests/tests/utils/config.py b/integration-tests/tests/utils/config.py index 75f97c4d46..97e78099ca 100644 --- a/integration-tests/tests/utils/config.py +++ b/integration-tests/tests/utils/config.py @@ -265,10 +265,10 @@ def _get_clp_instance_id(clp_instance_id_file_path: Path) -> str: err_msg = f"Cannot read instance-id file '{clp_instance_id_file_path}'" raise ValueError(err_msg) from err - if not re.fullmatch(r"[0-9a-fA-F]{4}", contents): + if not re.fullmatch(r"[0-9a-fA-F]{4}|[0-9a-fA-F]{32}|[0-9a-fA-F-]{36}", contents): err_msg = ( f"Invalid instance ID in {clp_instance_id_file_path}: expected a 4-character" - f" hexadecimal string, but read {contents}." + f" hexadecimal string or a full UUID, but read {contents}." ) raise ValueError(err_msg) From bbaafadc8c86d53763e2a20627c782f0535d552b Mon Sep 17 00:00:00 2001 From: Nathan <30803215+Nathan903@users.noreply.github.com> Date: Fri, 6 Mar 2026 16:18:26 -0500 Subject: [PATCH 27/27] chore: Bump chart version to 0.2.0-dev.3 --- tools/deployment/package-helm/Chart.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/deployment/package-helm/Chart.yaml b/tools/deployment/package-helm/Chart.yaml index 93c3ff99be..1f5a2288af 100644 --- a/tools/deployment/package-helm/Chart.yaml +++ b/tools/deployment/package-helm/Chart.yaml @@ -1,6 +1,6 @@ apiVersion: "v2" name: "clp" -version: "0.2.0-dev.2" +version: "0.2.0-dev.3" description: "A Helm chart for CLP's (Compressed Log Processor) package deployment" type: "application" appVersion: "0.9.1-dev"