Skip to content

Commit 4505f8c

Browse files
Nathan903junhaoliaoquinntaylormitchell
committed
feat(clp-package)!: Add telemetry consent, configuration, and docs. (#2251)
Co-authored-by: Junhao Liao <junhao@junhao.ca> Co-authored-by: Quinn Taylor Mitchell <q.mitchell@mail.utoronto.ca> Co-authored-by: Junhao Liao <junhao.liao@yscope.com>
1 parent a77327b commit 4505f8c

20 files changed

Lines changed: 401 additions & 43 deletions

File tree

components/clp-package-utils/clp_package_utils/controller.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1155,7 +1155,7 @@ def get_or_create_instance_id(clp_config: ClpConfig) -> str:
11551155
with open(resolved_instance_id_file_path, "r") as f:
11561156
instance_id = f.readline()
11571157
else:
1158-
instance_id = str(uuid.uuid4())[-4:]
1158+
instance_id = str(uuid.uuid4())
11591159
with open(resolved_instance_id_file_path, "w") as f:
11601160
f.write(instance_id)
11611161

components/clp-package-utils/clp_package_utils/general.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import enum
22
import errno
33
import json
4+
import logging
45
import os
56
import pathlib
67
import re
@@ -11,6 +12,7 @@
1112
from enum import auto
1213

1314
import yaml
15+
from ruamel.yaml import YAML
1416
from clp_py_utils.clp_config import (
1517
CLP_DEFAULT_CONFIG_FILE_RELATIVE_PATH,
1618
CLP_DEFAULT_CREDENTIALS_FILE_PATH,
@@ -54,6 +56,8 @@
5456
S3_KEY_PREFIX_COMPRESSION = "s3-key-prefix"
5557
S3_OBJECT_COMPRESSION = "s3-object"
5658

59+
logger = logging.getLogger(__name__)
60+
5761

5862
class DockerDependencyError(OSError):
5963
"""Base class for errors related to Docker dependencies."""
@@ -781,6 +785,49 @@ def _is_docker_compose_project_running(project_name: str) -> bool:
781785
) from e
782786

783787

788+
def set_yaml_key(file_path: pathlib.Path, key: str, value: str) -> None:
789+
"""
790+
Sets a possibly nested key in a YAML file, preserving formatting and comments.
791+
792+
:param file_path:
793+
:param key: Dot-separated key path (e.g., ``"telemetry.disable"``).
794+
:param value:
795+
"""
796+
ryaml = YAML()
797+
section, _, sub_key = key.partition(".")
798+
parsed_value = ryaml.load(value)
799+
800+
if file_path.exists():
801+
content = ryaml.load(file_path)
802+
if content is None:
803+
# File has only comments so can't round-trip through ruamel.yaml.
804+
# Append as text to preserve existing comments.
805+
text = file_path.read_text().rstrip("\n")
806+
if sub_key:
807+
text += f"\n\n{section}:\n {sub_key}: {value}\n"
808+
else:
809+
text += f"\n\n{key}: {value}\n"
810+
try:
811+
file_path.write_text(text)
812+
logger.info("Config updated in %s", file_path)
813+
except OSError:
814+
logger.warning("Failed to update config %s", file_path, exc_info=True)
815+
return
816+
else:
817+
content = {}
818+
819+
if sub_key:
820+
content.setdefault(section, {})[sub_key] = parsed_value
821+
else:
822+
content[section] = parsed_value
823+
824+
try:
825+
ryaml.dump(content, file_path)
826+
logger.info("Config updated in %s", file_path)
827+
except OSError:
828+
logger.warning("Failed to update config %s", file_path, exc_info=True)
829+
830+
784831
def _validate_data_directory(data_dir: pathlib.Path, component_name: str) -> None:
785832
try:
786833
validate_path_could_be_dir(resolve_host_path_in_container(data_dir))

components/clp-package-utils/clp_package_utils/scripts/start_clp.py

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,21 @@
22
"""Script to start the CLP Package."""
33

44
import logging
5+
import os
56
import pathlib
67
import sys
78

89
import click
9-
from clp_py_utils.clp_config import CLP_DEFAULT_CONFIG_FILE_RELATIVE_PATH
10+
11+
from clp_py_utils.clp_config import CLP_DEFAULT_CONFIG_FILE_RELATIVE_PATH, ClpConfig
1012
from clp_py_utils.core import resolve_host_path_in_container
1113

1214
from clp_package_utils.cli_utils import RESTART_POLICY
1315
from clp_package_utils.controller import DockerComposeController, get_or_create_instance_id
1416
from clp_package_utils.general import (
1517
get_clp_home,
1618
load_config_file,
19+
set_yaml_key,
1720
validate_and_load_db_credentials_file,
1821
validate_and_load_queue_credentials_file,
1922
validate_and_load_redis_credentials_file,
@@ -23,6 +26,29 @@
2326

2427
logger = logging.getLogger(__name__)
2528

29+
# Values accepted by both CLP_DISABLE_TELEMETRY and DO_NOT_TRACK to disable telemetry.
30+
_TELEMETRY_DISABLE_VALUES = ("1", "true", "yes", "y")
31+
32+
33+
TELEMETRY_NOTICE = """
34+
================================================================================
35+
CLP collects anonymous operational metrics to help improve the software. This
36+
includes: CLP version, OS/architecture, deployment method, bytes ingested,
37+
compression ratios, query volume, and more. It does NOT include: log content,
38+
queries, hostnames, IP addresses, or any other Personally Identifiable
39+
Information (PII).
40+
41+
Telemetry is sent to: https://telemetry.yscope.io
42+
For details, see: https://docs.yscope.com/clp/main/user-docs/reference-telemetry
43+
44+
You can disable metrics at any time by setting the environment variable
45+
CLP_DISABLE_TELEMETRY=true. Network admins can also block
46+
https://telemetry.yscope.io at the firewall level.
47+
================================================================================
48+
"""
49+
50+
TELEMETRY_PROMPT = "Enable anonymous telemetry to help improve CLP? [Y/n] "
51+
2652

2753
@click.command()
2854
@click.option(
@@ -85,6 +111,8 @@ def main(
85111
logger.exception("Failed to load config.")
86112
sys.exit(1)
87113

114+
_handle_telemetry_consent(clp_config, resolved_config_path)
115+
88116
try:
89117
# Create necessary directories.
90118
resolve_host_path_in_container(clp_config.data_directory).mkdir(parents=True, exist_ok=True)
@@ -115,5 +143,48 @@ def main(
115143
sys.exit(1)
116144

117145

146+
def _handle_telemetry_consent(clp_config: ClpConfig, config_file_path: pathlib.Path) -> None:
147+
"""
148+
Handles telemetry consent and prompts the user on first run if needed.
149+
150+
Priority order for handling telemetry preference:
151+
1. Session-only environment variable overrides. e.g.,
152+
a. CLP_DISABLE_TELEMETRY=true
153+
b. DO_NOT_TRACK=true
154+
2. Config file opt-out
155+
3. Previously confirmed telemetry preference
156+
4. First run consent prompt in interactive sessions
157+
158+
:param config_file_path: for persisting consent.
159+
"""
160+
clp_disable_val = os.environ.get("CLP_DISABLE_TELEMETRY", "").strip().lower()
161+
dnt_val = os.environ.get("DO_NOT_TRACK", "").strip().lower()
162+
if clp_disable_val in _TELEMETRY_DISABLE_VALUES or dnt_val in _TELEMETRY_DISABLE_VALUES:
163+
clp_config.telemetry.disable = True
164+
return
165+
166+
if clp_config.telemetry.disable:
167+
return
168+
169+
# Skip prompt if not the first run. i.e., telemetry preference has been confirmed previously.
170+
instance_id_file = clp_config.logs_directory / "instance-id"
171+
if resolve_host_path_in_container(instance_id_file).exists():
172+
return
173+
174+
if not sys.stdin.isatty():
175+
return
176+
177+
print(TELEMETRY_NOTICE)
178+
try:
179+
response = input(TELEMETRY_PROMPT).strip().lower()
180+
except EOFError:
181+
# e.g., Ctrl+D
182+
response = "n"
183+
184+
if response.startswith("n"):
185+
clp_config.telemetry.disable = True
186+
set_yaml_key(config_file_path, "telemetry.disable", "true")
187+
188+
118189
if "__main__" == __name__:
119190
main()

components/clp-package-utils/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ dependencies = [
1313
"pydantic>=2.12.5",
1414
"pymongo>=4.16.0",
1515
"PyYAML>=6.0.3",
16+
"ruamel.yaml>=0.19.1",
1617
]
1718

1819
[build-system]

0 commit comments

Comments
 (0)