config.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. # Copyright 2024 The Chromium Authors
  2. # Use of this source code is governed by a BSD-style license that can be
  3. # found in the LICENSE file.
  4. """Provides telemetry configuration utilities."""
  5. import configparser
  6. import datetime
  7. import os
  8. from pathlib import Path
  9. import shutil
  10. import tempfile
  11. import time
  12. from typing import Literal
  13. import uuid
  14. DEFAULT_CONFIG_FILE = Path(Path.home(),
  15. '.config/chrome_infra/telemetry/telemetry.cfg')
  16. ROOT_SECTION_KEY = "root"
  17. NOTICE_COUNTDOWN_KEY = "notice_countdown"
  18. ENABLED_KEY = "enabled"
  19. ENABLED_REASON_KEY = "enabled_reason"
  20. TRACE_SECTION_KEY = "trace"
  21. DEFAULT_CONFIG = {
  22. ROOT_SECTION_KEY: {
  23. NOTICE_COUNTDOWN_KEY: 10
  24. },
  25. TRACE_SECTION_KEY: {},
  26. }
  27. BATCH_PUBLISHING_ENABLED_KEY = "batch_publishing"
  28. # The "telemetry in development" config to allow publishing the telemetry, but
  29. # easily filtering it out later.
  30. KEY_DEV = "development"
  31. # Can be set, but the value is unused, pending approvals.
  32. KEY_USER_UUID = "user_uuid"
  33. KEY_USER_UUID_TIMESTAMP = "user_uuid_generated"
  34. class TraceConfig:
  35. """Tracing specific config in Telemetry config."""
  36. def __init__(self, config: configparser.ConfigParser) -> None:
  37. self._config = config
  38. def update(self, enabled: bool, reason: Literal["AUTO", "USER"]) -> None:
  39. """Update the config."""
  40. self._config[TRACE_SECTION_KEY][ENABLED_KEY] = str(enabled)
  41. self._config[TRACE_SECTION_KEY][ENABLED_REASON_KEY] = reason
  42. if enabled:
  43. self.gen_id()
  44. def gen_id(self, regen=False) -> None:
  45. """[Re]generate UUIDs."""
  46. if regen or self._uuid_stale():
  47. self._config[TRACE_SECTION_KEY][KEY_USER_UUID] = str(uuid.uuid4())
  48. self._config[TRACE_SECTION_KEY][KEY_USER_UUID_TIMESTAMP] = str(
  49. int(time.time()))
  50. @property
  51. def batch(self) -> bool:
  52. """Check if batch uploads are configured."""
  53. return self._config[TRACE_SECTION_KEY].getboolean(
  54. BATCH_PUBLISHING_ENABLED_KEY, False)
  55. @batch.setter
  56. def batch(self, enabled: bool) -> None:
  57. """Set or delete the batch flag."""
  58. self._config[TRACE_SECTION_KEY][BATCH_PUBLISHING_ENABLED_KEY] = str(
  59. enabled)
  60. def _uuid_stale(self):
  61. """Check if the UUID is stale or doesn't exist."""
  62. if (KEY_USER_UUID not in self._config[TRACE_SECTION_KEY] or
  63. KEY_USER_UUID_TIMESTAMP not in self._config[TRACE_SECTION_KEY]):
  64. return True
  65. # Regen the UUID once per week. Regen every Monday so the work week is
  66. # captured under a single ID.
  67. regen_ts = int(self._config[TRACE_SECTION_KEY][KEY_USER_UUID_TIMESTAMP])
  68. regen_dt = datetime.datetime.fromtimestamp(regen_ts)
  69. today = datetime.datetime.now().replace(hour=0,
  70. minute=0,
  71. second=0,
  72. microsecond=0)
  73. monday = today - datetime.timedelta(days=today.weekday())
  74. return regen_dt < monday
  75. def has_enabled(self) -> bool:
  76. """Checks if the enabled property exists in config."""
  77. return ENABLED_KEY in self._config[TRACE_SECTION_KEY]
  78. @property
  79. def enabled(self) -> bool:
  80. """Value of trace.enabled property in telemetry.cfg."""
  81. return self._config[TRACE_SECTION_KEY].getboolean(ENABLED_KEY, False)
  82. @property
  83. def enabled_reason(self) -> Literal["AUTO", "USER"]:
  84. """Value of trace.enabled_reason property in telemetry.cfg."""
  85. return self._config[TRACE_SECTION_KEY].get(ENABLED_REASON_KEY, "AUTO")
  86. @property
  87. def dev_flag(self):
  88. """Check the telemetry development flag."""
  89. return self._config[TRACE_SECTION_KEY].getboolean(KEY_DEV, False)
  90. @dev_flag.setter
  91. def dev_flag(self, enabled: bool) -> None:
  92. """Set or delete the development flag."""
  93. if enabled:
  94. self._config[TRACE_SECTION_KEY][KEY_DEV] = str(enabled)
  95. elif KEY_DEV in self._config[TRACE_SECTION_KEY]:
  96. del self._config[TRACE_SECTION_KEY][KEY_DEV]
  97. def user_uuid(self) -> str:
  98. """Get the user UUID value."""
  99. return self._config[TRACE_SECTION_KEY].get(KEY_USER_UUID, "")
  100. class RootConfig:
  101. """Root configs in Telemetry config."""
  102. def __init__(self, config) -> None:
  103. self._config = config
  104. def update(self, notice_countdown: int) -> None:
  105. """Update the config."""
  106. self._config[ROOT_SECTION_KEY][NOTICE_COUNTDOWN_KEY] = str(
  107. notice_countdown)
  108. @property
  109. def notice_countdown(self) -> int:
  110. """Value for root.notice_countdown property in telemetry.cfg."""
  111. return self._config[ROOT_SECTION_KEY].getint(NOTICE_COUNTDOWN_KEY, 10)
  112. class Config:
  113. """Telemetry configuration."""
  114. def __init__(self, path: os.PathLike) -> None:
  115. self._path = Path(path)
  116. self._config = configparser.ConfigParser()
  117. self._config.read_dict(DEFAULT_CONFIG)
  118. if not self._path.exists():
  119. self.flush()
  120. else:
  121. with self._path.open("r", encoding="utf-8") as configfile:
  122. self._config.read_file(configfile)
  123. self._trace_config = TraceConfig(self._config)
  124. self._root_config = RootConfig(self._config)
  125. def flush(self) -> None:
  126. """Flushes the current config to config file."""
  127. tmpfile = tempfile.NamedTemporaryFile()
  128. with open(tmpfile.name, "w", encoding="utf-8") as configfile:
  129. self._config.write(configfile)
  130. if not self._path.parent.exists():
  131. self._path.parent.mkdir(parents=True, exist_ok=True)
  132. shutil.copy(tmpfile.name, self._path)
  133. @property
  134. def root_config(self) -> RootConfig:
  135. """The root config in telemetry."""
  136. return self._root_config
  137. @property
  138. def trace_config(self) -> TraceConfig:
  139. """The trace config in telemetry."""
  140. return self._trace_config