Skip to content

Commit 14130d4

Browse files
bmehta001Copilot
andcommitted
Address telemetry review comments
Avoid duplicate OliveRecipe telemetry from Docker workflows by suppressing inner workflow recipe events and forwarding CI detection into workflow containers. Keep CI telemetry ephemeral by skipping cache setup, and make recipe metadata stable by avoiding filesystem-sensitive model/resource classification. Files changed: - docs/Privacy.md - olive/systems/docker/docker_system.py - olive/telemetry/constants.py - olive/telemetry/telemetry.py - olive/workflows/run/run.py - test/systems/docker/test_docker_system.py - test/test_telemetry.py - test/workflows/test_workflow_run.py Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 1cae73f commit 14130d4

8 files changed

Lines changed: 149 additions & 36 deletions

File tree

docs/Privacy.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,4 @@ In addition, Olive may collect additional telemetry data such as:
1313
- Performance data
1414
- Exception information
1515

16-
Collection of this additional telemetry can be disabled by adding the `--disable_telemetry` flag to any Olive CLI command, or by setting the `OLIVE_DISABLE_TELEMETRY` environment variable to `1` before running. In CI/CD environments (e.g., GitHub Actions, Azure Pipelines, Jenkins), Olive suppresses the general heartbeat/action/error events and only emits the `OliveRecipe` event. The `OliveRecipe` event may include recipe metadata such as pass types, explicit target and host settings, whether a custom package config was provided, a redacted snapshot of custom package-config overrides, and a redacted snapshot of explicitly supplied config overrides. If telemetry is enabled, but cannot be sent to Microsoft, it will be stored locally and sent when a connection is available. You can override the default cache location by setting the `OLIVE_TELEMETRY_CACHE_DIR` environment variable to a valid directory path.
16+
Collection of this additional telemetry can be disabled by adding the `--disable_telemetry` flag to any Olive CLI command, or by setting the `OLIVE_DISABLE_TELEMETRY` environment variable to `1` before running. In CI/CD environments (e.g., GitHub Actions, Azure Pipelines, Jenkins), Olive suppresses the general heartbeat/action/error events and only emits the `OliveRecipe` event. The `OliveRecipe` event may include recipe metadata such as pass types, explicitly configured target settings, the host system type and any explicitly configured host accelerator settings, whether a custom package config was provided, a redacted snapshot of custom package-config overrides, and a redacted snapshot of explicitly supplied config overrides. Outside CI/CD environments, if telemetry is enabled but cannot be sent to Microsoft, it will be stored locally and sent when a connection is available. You can override the default cache location by setting the `OLIVE_TELEMETRY_CACHE_DIR` environment variable to a valid directory path.

olive/systems/docker/docker_system.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import copy
66
import json
77
import logging
8+
import os
89
import sys
910
import tempfile
1011
from pathlib import Path
@@ -19,6 +20,8 @@
1920
from olive.systems.common import AcceleratorConfig, SystemType
2021
from olive.systems.olive_system import OliveSystem
2122
from olive.systems.system_config import LocalTargetUserConfig, SystemConfig
23+
from olive.telemetry.constants import SUPPRESS_WORKFLOW_TELEMETRY_ENV
24+
from olive.telemetry.telemetry import is_ci_environment
2225
from olive.workflows.run.config import RunConfig
2326

2427
if TYPE_CHECKING:
@@ -241,6 +244,9 @@ def _prepare_environment(self, base_env) -> dict:
241244
# Add default environment variables
242245
environment.setdefault("PYTHONPYCACHEPREFIX", "/tmp")
243246
environment["OLIVE_LOG_LEVEL"] = logging.getLevelName(logger.getEffectiveLevel())
247+
environment[SUPPRESS_WORKFLOW_TELEMETRY_ENV] = "1"
248+
if is_ci_environment():
249+
environment["CI"] = "1"
244250

245251
# Add HuggingFace token if needed
246252
if self.hf_token:
@@ -303,8 +309,6 @@ def _create_runner_script_mount(self) -> tuple[str, str]:
303309
@staticmethod
304310
def _get_huggingface_token() -> Optional[str]:
305311
"""Get HuggingFace token from environment or file."""
306-
import os
307-
308312
# Check environment variable
309313
token = os.getenv("HF_TOKEN")
310314
if token:

olive/telemetry/constants.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# Licensed under the MIT License.
44
# --------------------------------------------------------------------------
55

6-
"""OneCollector connection string."""
6+
"""Telemetry constants."""
77

88
CONNECTION_STRING = "SW5zdHJ1bWVudGF0aW9uS2V5PTYyMTUwOTExZGMwMDRmYzliYjY3YmE5NjA2NDI3ZTU2LWVjNjFmOWFmLTVkN2EtNGQxOS1hZjMxLWI5Y2Q2OWU5ODdmMS02OTE1"
9+
SUPPRESS_WORKFLOW_TELEMETRY_ENV = "OLIVE_SUPPRESS_WORKFLOW_TELEMETRY"

olive/telemetry/telemetry.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -473,11 +473,11 @@ def __init__(self):
473473
self._logger = self._create_logger()
474474
event_source.disable()
475475

476-
self._cache_handler = TelemetryCacheHandler(self)
477-
self._setup_payload_callbacks()
478476
is_ci = self._is_ci_environment()
479477
self._recipe_only_ci_telemetry = is_ci
480478
if not is_ci:
479+
self._cache_handler = TelemetryCacheHandler(self)
480+
self._setup_payload_callbacks()
481481
self._log_heartbeat()
482482
if os.environ.get("OLIVE_DISABLE_TELEMETRY") == "1":
483483
self.disable_telemetry()
@@ -500,7 +500,7 @@ def _create_logger(self) -> Optional[TelemetryLogger]:
500500
def _setup_payload_callbacks(self) -> None:
501501
# Register callback for payload transmission events
502502
# No need to store unregister function - logger shutdown will clean up callbacks
503-
if self._logger is None:
503+
if self._logger is None or self._cache_handler is None:
504504
return
505505
self._logger.register_payload_transmitted_callback(
506506
self._cache_handler.on_payload_transmitted,

olive/workflows/run/run.py

Lines changed: 28 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,20 @@
66
import importlib
77
import json
88
import logging
9+
import os
910
from copy import deepcopy
1011
from os import PathLike
11-
from pathlib import Path
12+
from pathlib import Path, PurePosixPath, PureWindowsPath
1213
from typing import TYPE_CHECKING, Any, Optional, Union
1314

1415
from olive.common.config_utils import load_config_file
1516
from olive.common.utils import hash_dict, set_tempdir
1617
from olive.hardware.constants import ExecutionProvider
1718
from olive.logging import set_default_logger_severity, set_ort_logger_severity, set_verbosity_info
1819
from olive.package_config import OlivePackageConfig
19-
from olive.resource_path import create_resource_path, find_all_resources
2020
from olive.systems.accelerator_creator import create_accelerator
2121
from olive.systems.common import SystemType
22+
from olive.telemetry.constants import SUPPRESS_WORKFLOW_TELEMETRY_ENV
2223
from olive.telemetry.telemetry import is_ci_environment
2324
from olive.telemetry.telemetry_extensions import log_recipe_result
2425
from olive.workflows.run.config import RunConfig
@@ -227,7 +228,7 @@ def run(
227228

228229
if parsed_run_config.engine.host and parsed_run_config.engine.host.type == SystemType.Docker:
229230
docker_system = parsed_run_config.engine.host.create_system()
230-
workflow_output = docker_system.run_workflow(parsed_run_config)
231+
workflow_output = docker_system.run_workflow(deepcopy(parsed_run_config))
231232
success = True
232233
return workflow_output
233234

@@ -240,17 +241,18 @@ def run(
240241
exception_type = type(exc).__name__
241242
raise
242243
finally:
243-
metadata = _build_recipe_result_metadata(
244-
run_config,
245-
run_config_telemetry_input,
246-
parsed_run_config,
247-
recipe_telemetry_metadata,
248-
list_required_packages=list_required_packages,
249-
package_config_input=package_config_telemetry_input,
250-
package_config_provided=package_config_provided,
251-
)
252-
recipe_name = metadata.pop("recipe_name")
253-
log_recipe_result(recipe_name, success=success, metadata=metadata, exception_type=exception_type)
244+
if os.environ.get(SUPPRESS_WORKFLOW_TELEMETRY_ENV) != "1":
245+
metadata = _build_recipe_result_metadata(
246+
run_config,
247+
run_config_telemetry_input,
248+
parsed_run_config,
249+
recipe_telemetry_metadata,
250+
list_required_packages=list_required_packages,
251+
package_config_input=package_config_telemetry_input,
252+
package_config_provided=package_config_provided,
253+
)
254+
recipe_name = metadata.pop("recipe_name")
255+
log_recipe_result(recipe_name, success=success, metadata=metadata, exception_type=exception_type)
254256

255257

256258
def generate_files_from_packages(packages, file_name):
@@ -510,12 +512,20 @@ def _classify_input_model_source(model_identifier: Any) -> str:
510512
if identifier.startswith(("http://", "https://")):
511513
return "url"
512514

513-
resource_path = create_resource_path(identifier)
514-
if resource_path.is_local_resource():
515-
return "local_file" if resource_path.type.value == "file" else "local_folder"
515+
if _is_explicit_local_model_path(identifier):
516+
suffix = PureWindowsPath(identifier).suffix or PurePosixPath(identifier).suffix
517+
return "local_file" if suffix else "local_folder"
516518
return "string_name"
517519

518520

521+
def _is_explicit_local_model_path(identifier: str) -> bool:
522+
return (
523+
identifier.startswith(("./", "../", ".\\", "..\\", "~/", "~\\", "/", "\\\\"))
524+
or PureWindowsPath(identifier).is_absolute()
525+
or PurePosixPath(identifier).is_absolute()
526+
)
527+
528+
519529
def _extract_target_metadata(run_config: RunConfig) -> dict[str, Optional[str]]:
520530
target_system = run_config.engine.target
521531
return _extract_system_metadata(target_system, "target")
@@ -562,13 +572,11 @@ def _set_metadata_if_present(metadata: dict[str, Any], values: dict[str, Optiona
562572
def _build_recipe_hash(run_config_json: dict[str, Any]) -> str:
563573
sanitized = deepcopy(run_config_json)
564574
_redact_recipe_hash_keys(sanitized)
565-
for path in find_all_resources(sanitized):
566-
_set_path_value(sanitized, path, RECIPE_HASH_REDACTED_VALUE)
567575
return hash_dict(sanitized)[:16]
568576

569577

570578
def _redact_recipe_hash_keys(value: Any, key: Optional[str] = None) -> Any:
571-
if key in RECIPE_HASH_REDACTED_KEYS:
579+
if key in RECIPE_HASH_REDACTED_KEYS or _is_path_like_key(key):
572580
return RECIPE_HASH_REDACTED_VALUE
573581
if isinstance(value, dict):
574582
for child_key in list(value):
@@ -577,10 +585,3 @@ def _redact_recipe_hash_keys(value: Any, key: Optional[str] = None) -> Any:
577585
for index, item in enumerate(value):
578586
value[index] = _redact_recipe_hash_keys(item, key)
579587
return value
580-
581-
582-
def _set_path_value(container: Any, path: tuple[Any, ...], value: Any) -> None:
583-
current = container
584-
for key in path[:-1]:
585-
current = current[key]
586-
current[path[-1]] = value

test/systems/docker/test_docker_system.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from olive.systems.docker.docker_system import DockerSystem
1010
from olive.systems.system_config import DockerTargetUserConfig
11+
from olive.telemetry.constants import SUPPRESS_WORKFLOW_TELEMETRY_ENV
1112
from test.utils import ONNX_MODEL_PATH
1213

1314
# pylint: disable=attribute-defined-outside-init,protected-access
@@ -136,10 +137,30 @@ def test_run_workflow(self, mock_find_resources, mock_tempdir, mock_from_env, tm
136137
command = mock_docker_client.containers.run.call_args[1]["command"]
137138
assert "workflow_runner.py" in command
138139
assert "--config" in command
140+
assert mock_docker_client.containers.run.call_args.kwargs["environment"][SUPPRESS_WORKFLOW_TELEMETRY_ENV] == "1"
139141

140142
# Verify cleanup
141143
mock_container.remove.assert_called_once()
142144

145+
@patch("olive.systems.docker.docker_system.docker.from_env")
146+
def test_prepare_environment_forwards_ci_to_workflow_container(self, mock_from_env, monkeypatch):
147+
mock_docker_client = MagicMock()
148+
mock_from_env.return_value = mock_docker_client
149+
mock_docker_client.images.get.return_value = MagicMock()
150+
monkeypatch.setenv("TF_BUILD", "True")
151+
docker_config = self.get_default_docker_config()
152+
docker_system = DockerSystem(
153+
image_name=docker_config.image_name,
154+
build_context_path=docker_config.build_context_path,
155+
dockerfile=docker_config.dockerfile,
156+
work_dir=docker_config.work_dir,
157+
)
158+
159+
environment = docker_system._prepare_environment({})
160+
161+
assert environment["CI"] == "1"
162+
assert environment[SUPPRESS_WORKFLOW_TELEMETRY_ENV] == "1"
163+
143164
@patch("olive.systems.docker.docker_system.docker.from_env")
144165
@patch("olive.systems.docker.docker_system.tempfile.TemporaryDirectory")
145166
@patch("olive.systems.docker.docker_system.find_all_resources")

test/test_telemetry.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ def test_telemetry_only_logs_recipe_events_in_ci(monkeypatch):
5555

5656
assert mock_logger.log.call_count == 1
5757
assert mock_logger.log.call_args.args[0] == RECIPE_EVENT_NAME
58+
assert telemetry._cache_handler is None
59+
mock_logger.register_payload_transmitted_callback.assert_not_called()
5860
finally:
5961
Telemetry._instance = None
6062

test/workflows/test_workflow_run.py

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22
import sys
33
from copy import deepcopy
44
from pathlib import Path
5-
from unittest.mock import patch
5+
from unittest.mock import Mock, patch
66

77
import pytest
88

9+
from olive.telemetry.constants import SUPPRESS_WORKFLOW_TELEMETRY_ENV
910
from olive.workflows import run as olive_run
10-
from olive.workflows.run.run import _classify_run_config_source
11+
from olive.workflows.run.run import _build_recipe_hash, _classify_input_model_source, _classify_run_config_source
1112
from test.utils import (
1213
get_pytorch_model,
1314
get_pytorch_model_config,
@@ -229,6 +230,66 @@ def test_run_logs_recipe_result_failure(mock_run_engine, mock_log_recipe_result)
229230
assert mock_log_recipe_result.call_args.kwargs["exception_type"] == "ValueError"
230231

231232

233+
@patch("olive.workflows.run.run.log_recipe_result")
234+
@patch("olive.workflows.run.run.run_engine")
235+
def test_run_skips_recipe_result_when_workflow_telemetry_is_suppressed(
236+
mock_run_engine, mock_log_recipe_result, monkeypatch
237+
):
238+
monkeypatch.setenv(SUPPRESS_WORKFLOW_TELEMETRY_ENV, "1")
239+
expected_output = object()
240+
mock_run_engine.return_value = expected_output
241+
242+
output = olive_run(
243+
{
244+
"input_model": {
245+
"type": "HfModel",
246+
"model_path": "Qwen/Qwen2.5-0.5B-Instruct",
247+
"task": "text-generation",
248+
}
249+
}
250+
)
251+
252+
assert output is expected_output
253+
mock_log_recipe_result.assert_not_called()
254+
255+
256+
@patch("olive.workflows.run.run.log_recipe_result")
257+
@patch("olive.systems.system_config.SystemConfig.create_system")
258+
def test_run_logs_single_parent_recipe_result_for_docker_host(mock_create_system, mock_log_recipe_result):
259+
expected_output = object()
260+
docker_system = Mock()
261+
262+
def run_workflow(container_run_config):
263+
container_run_config.engine.host = container_run_config.engine.target
264+
return expected_output
265+
266+
docker_system.run_workflow.side_effect = run_workflow
267+
mock_create_system.return_value = docker_system
268+
config = {
269+
"input_model": {"type": "ONNXModel", "model_path": "model.onnx"},
270+
"systems": {
271+
"docker_system": {
272+
"type": "Docker",
273+
"config": {
274+
"dockerfile": "Dockerfile",
275+
"build_context_path": "build_context",
276+
"image_name": "test-image:latest",
277+
"work_dir": "/olive-ws",
278+
},
279+
},
280+
"local_system": {"type": "LocalSystem"},
281+
},
282+
"engine": {"host": "docker_system", "target": "local_system"},
283+
}
284+
285+
output = olive_run(config)
286+
287+
assert output is expected_output
288+
mock_log_recipe_result.assert_called_once()
289+
metadata = mock_log_recipe_result.call_args.kwargs["metadata"]
290+
assert metadata["host_system_type"] == "Docker"
291+
292+
232293
@patch("olive.workflows.run.run.log_recipe_result")
233294
@patch("olive.workflows.run.run.run_engine")
234295
def test_run_logs_recipe_host_metadata_without_explicit_target(mock_run_engine, mock_log_recipe_result):
@@ -312,3 +373,26 @@ def test_run_logs_package_config_overrides_when_package_config_provided(mock_run
312373

313374
def test_classify_run_config_source_handles_non_pathlike_object():
314375
assert _classify_run_config_source(object()) == ("config_object", "object")
376+
377+
378+
def test_classify_input_model_source_does_not_depend_on_local_filesystem(tmp_path, monkeypatch):
379+
assert _classify_input_model_source("Qwen/Qwen2.5-0.5B-Instruct") == "string_name"
380+
381+
monkeypatch.chdir(tmp_path)
382+
(tmp_path / "bert-base-uncased").mkdir()
383+
384+
assert _classify_input_model_source("bert-base-uncased") == "string_name"
385+
assert _classify_input_model_source("./model.onnx") == "local_file"
386+
387+
388+
def test_recipe_hash_does_not_depend_on_local_model_path_presence(tmp_path, monkeypatch):
389+
config = {
390+
"input_model": {"type": "HfModel", "config": {"model_path": "bert-base-uncased"}},
391+
"engine": {"output_dir": "output"},
392+
}
393+
recipe_hash = _build_recipe_hash(config)
394+
395+
monkeypatch.chdir(tmp_path)
396+
(tmp_path / "bert-base-uncased").mkdir()
397+
398+
assert _build_recipe_hash(config) == recipe_hash

0 commit comments

Comments
 (0)