blob: 74a72d3d656223dbde3e53d75fff71b861d3342f [file] [log] [blame]
# Copyright 2023 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Provides telemetry configuration utilities."""
import configparser
import datetime
import os
from pathlib import Path
import time
from typing import Literal
import uuid
from chromite.lib import cros_build_lib
ROOT_SECTION_KEY = "root"
NOTICE_COUNTDOWN_KEY = "notice_countdown"
ENABLED_KEY = "enabled"
ENABLED_REASON_KEY = "enabled_reason"
TRACE_SECTION_KEY = "trace"
DEFAULT_CONFIG = {
ROOT_SECTION_KEY: {NOTICE_COUNTDOWN_KEY: 10},
TRACE_SECTION_KEY: {},
}
# The "telemetry in development" config to allow publishing the telemetry, but
# easily filtering it out later.
KEY_DEV = "development"
# Can be set, but the value is unused, pending approvals.
KEY_USER_UUID = "user_uuid"
KEY_USER_UUID_TIMESTAMP = "user_uuid_generated"
class TraceConfig:
"""Tracing specific config in Telemetry config."""
def __init__(self, config: configparser.ConfigParser) -> None:
self._config = config
def update(self, enabled: bool, reason: Literal["AUTO", "USER"]) -> None:
"""Update the config."""
self._config.set(TRACE_SECTION_KEY, ENABLED_KEY, str(enabled))
self._config.set(TRACE_SECTION_KEY, ENABLED_REASON_KEY, reason)
if enabled:
self.gen_id()
def set_dev(self, enabled: bool) -> None:
"""Set or delete the development flag."""
if enabled:
self._config.set(TRACE_SECTION_KEY, KEY_DEV, str(enabled))
elif KEY_DEV in self._config[TRACE_SECTION_KEY]:
del self._config[TRACE_SECTION_KEY][KEY_DEV]
def gen_id(self, regen=False) -> bool:
"""[Re]generate UUIDs."""
if self.enabled and (regen or self._uuid_stale()):
self._config.set(
TRACE_SECTION_KEY, KEY_USER_UUID, str(uuid.uuid4())
)
self._config.set(
TRACE_SECTION_KEY,
KEY_USER_UUID_TIMESTAMP,
str(int(time.time())),
)
return True
return False
def _uuid_stale(self):
"""Check if the UUID is stale or doesn't exist."""
if (
KEY_USER_UUID not in self._config[TRACE_SECTION_KEY]
or KEY_USER_UUID_TIMESTAMP not in self._config[TRACE_SECTION_KEY]
):
return True
# Regen the UUID once per week. Regen every Monday so the work week is
# captured under a single ID.
regen_ts = int(self._config[TRACE_SECTION_KEY][KEY_USER_UUID_TIMESTAMP])
regen_dt = datetime.datetime.fromtimestamp(regen_ts)
today = datetime.datetime.now().replace(
hour=0, minute=0, second=0, microsecond=0
)
monday = today - datetime.timedelta(days=today.weekday())
return regen_dt < monday
def has_enabled(self) -> bool:
"""Checks if the enabled property exists in config."""
return ENABLED_KEY in self._config[TRACE_SECTION_KEY]
@property
def enabled(self) -> bool:
"""Value of trace.enabled property in telemetry.cfg."""
return self._config[TRACE_SECTION_KEY].getboolean(ENABLED_KEY, False)
@property
def enabled_reason(self) -> Literal["AUTO", "USER"]:
"""Value of trace.enabled_reason property in telemetry.cfg."""
return self._config[TRACE_SECTION_KEY].get(ENABLED_REASON_KEY, "AUTO")
@property
def dev_flag(self):
"""Check the telemetry development flag."""
return self._config[TRACE_SECTION_KEY].getboolean(KEY_DEV, False)
def user_uuid(self) -> str:
"""Get the user UUID value."""
return self._config[TRACE_SECTION_KEY].get(KEY_USER_UUID, "")
class RootConfig:
"""Root configs in Telemetry config."""
def __init__(self, config) -> None:
self._config = config
def update(self, notice_countdown: int) -> None:
"""Update the config."""
self._config.set(
ROOT_SECTION_KEY, NOTICE_COUNTDOWN_KEY, str(notice_countdown)
)
@property
def notice_countdown(self) -> int:
"""Value for root.notice_countdown property in telemetry.cfg."""
return self._config[ROOT_SECTION_KEY].getint(NOTICE_COUNTDOWN_KEY, 10)
class Config:
"""Telemetry configuration."""
def __init__(self, path: os.PathLike) -> None:
self._path = Path(path)
self._config = configparser.ConfigParser()
self._config.read_dict(DEFAULT_CONFIG)
if not self._path.exists():
self.flush()
else:
with self._path.open("r", encoding="utf-8") as configfile:
self._config.read_file(configfile)
self._trace_config = TraceConfig(self._config)
self._root_config = RootConfig(self._config)
def flush(self) -> None:
"""Flushes the current config to config file."""
tempfile = self._path.with_name(
f".tmp-{cros_build_lib.GetRandomString()}"
)
with tempfile.open("w", encoding="utf-8") as configfile:
self._config.write(configfile)
tempfile.rename(self._path)
@property
def root_config(self) -> RootConfig:
"""The root config in telemetry."""
return self._root_config
@property
def trace_config(self) -> TraceConfig:
"""The trace config in telemetry."""
return self._trace_config