Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
87 commits
Select commit Hold shift + click to select a range
566a6c5
Consolidate TelemetryCacheHandler to single lock
bmehta001 Apr 9, 2026
2211d19
Fix concurrency issues in telemetry
bmehta001 Apr 9, 2026
276431e
Simplify telemetry cache and singleton patterns
bmehta001 Apr 9, 2026
cf1d641
Fix callback double-counting and multi-process cache overwrite
bmehta001 Apr 10, 2026
7d78e14
Track recipe telemetry in CI
bmehta001 May 4, 2026
c301071
Remove avoidable telemetry lint suppressions
bmehta001 May 4, 2026
d2dd0f3
Move service name handling into telemetry logger
bmehta001 May 4, 2026
c08c9f0
Scope service-name cleanup to Olive usage
bmehta001 May 4, 2026
d51a297
Revert unnecessary non-telemetry branch changes
bmehta001 May 4, 2026
056bf20
Address PR review feedback on telemetry changes
bmehta001 May 4, 2026
00e4076
Remove low-value service-name telemetry test
bmehta001 May 4, 2026
f07c49f
Address remaining GitHub Advanced Security comments
bmehta001 May 4, 2026
702bacf
Simplify telemetry utils responsibilities
bmehta001 May 4, 2026
6dd7a05
Store CI detection result once in telemetry init
bmehta001 May 4, 2026
c219866
Refine recipe telemetry semantics and config tracking
bmehta001 May 4, 2026
43afc4d
Replace package config hash with override values
bmehta001 May 4, 2026
0a17cb7
Guard Azure CI secret-dependent login steps
bmehta001 May 5, 2026
e2a9b21
Revert Azure CI secret login guards
bmehta001 May 5, 2026
4e1ccda
Address telemetry review comments
bmehta001 May 5, 2026
2577278
Fix telemetry pipeline test regressions
bmehta001 May 5, 2026
cbb3073
Log workflow exceptions as error telemetry
bmehta001 May 9, 2026
4c70c46
Reduce CLI import churn
bmehta001 May 9, 2026
28ef5b0
Simplify Docker recipe telemetry suppression
bmehta001 May 9, 2026
a99ef15
Keep platform imports local
bmehta001 May 9, 2026
8c1c726
Move recipe telemetry helpers out of runner
bmehta001 May 11, 2026
3717d9f
Move recipe telemetry helpers to telemetry package
bmehta001 May 11, 2026
b50423c
Address simple telemetry review comments
bmehta001 May 21, 2026
a756445
Refine recipe telemetry metadata
bmehta001 May 22, 2026
35d3552
Harden telemetry cache replay and flush wait
bmehta001 May 26, 2026
7347cf8
Migrate telemetry to stdlib SQLite pipeline with three-state opt-out
bmehta001 Jun 26, 2026
aca051b
Apply multi-agent review fixes: init race, redaction, privacy doc
bmehta001 Jun 26, 2026
a757f07
Make the device-id heartbeat durable for reliable device counting
bmehta001 Jun 26, 2026
9c33f0a
Align heartbeat field names with onnxruntime-genai
bmehta001 Jun 26, 2026
db4efc0
Keep telemetry inert in tests after durable-heartbeat change
bmehta001 Jun 26, 2026
a07ec96
Harden device-id store to owner-only permissions
bmehta001 Jul 7, 2026
8acd353
telemetry: address Copilot review on shutdown and recipe logging
bmehta001 Jul 15, 2026
57af124
telemetry: address Copilot opt-out isolation feedback
bmehta001 Jul 15, 2026
0b80b96
telemetry: address Copilot shutdown and duplicate-error feedback
bmehta001 Jul 15, 2026
fb3889b
telemetry: address Copilot uploader follow-ups
bmehta001 Jul 15, 2026
9b64bf0
telemetry: address Copilot lint follow-ups
bmehta001 Jul 15, 2026
1901806
telemetry: keep uploader reference until shutdown
bmehta001 Jul 15, 2026
fcec32d
telemetry: address Copilot finalizer and metadata feedback
bmehta001 Jul 15, 2026
3d3abfe
telemetry: address Copilot uploader drain edge cases
bmehta001 Jul 15, 2026
a3df0d2
telemetry: avoid releasing drain lock while uploader runs
bmehta001 Jul 15, 2026
cf6e695
telemetry: fix local review findings before Copilot loop
bmehta001 Jul 20, 2026
ff6f96e
Merge upstream/main into Olive telemetry branch
bmehta001 Jul 20, 2026
ea884a3
telemetry: address Copilot round 1
bmehta001 Jul 20, 2026
a06dcfb
telemetry: address Copilot round 2 fixture cleanup
bmehta001 Jul 20, 2026
0a9b557
telemetry: satisfy Python format checks
bmehta001 Jul 20, 2026
8836517
telemetry: satisfy Pylint checks
bmehta001 Jul 20, 2026
9584988
telemetry: guard empty permission paths
bmehta001 Jul 20, 2026
f6b2917
telemetry: address Copilot uploader wake race
bmehta001 Jul 20, 2026
7e02760
telemetry: use monotonic flush deadlines
bmehta001 Jul 21, 2026
518df1e
telemetry: address Copilot recipe metadata round
bmehta001 Jul 21, 2026
08f70d4
telemetry: close completed review threads
bmehta001 Jul 21, 2026
c333feb
Bound Olive telemetry lifecycle work
bmehta001 Jul 21, 2026
b521622
Align Olive telemetry tests with CI behavior
bmehta001 Jul 21, 2026
389858a
Harden Olive device ID and traceback handling
bmehta001 Jul 21, 2026
1eaa239
Limit Olive device ID registry access
bmehta001 Jul 21, 2026
fd296dc
Preserve safe Olive traceback context
bmehta001 Jul 21, 2026
ae89069
Reuse the Olive device store test import
bmehta001 Jul 21, 2026
8e3fa6b
Label Olive actions from function ownership
bmehta001 Jul 21, 2026
06bed4b
Keep the Olive telemetry value at its use site
bmehta001 Jul 21, 2026
68a7106
Scope Olive telemetry suppression to each run
bmehta001 Jul 21, 2026
01e0fdd
Keep Olive exporter diagnostics configurable
bmehta001 Jul 21, 2026
c94d15b
Use the ORT telemetry opt-out contract in Olive
bmehta001 Jul 21, 2026
93e1150
Recognize installed Olive traceback frames
bmehta001 Jul 21, 2026
6059701
Match ONNX Runtime telemetry redaction
bmehta001 Jul 21, 2026
3cd536c
Keep ORT telemetry disabled for the full Olive command
bmehta001 Jul 21, 2026
6afbfa3
Scrub Olive action metadata recursively
bmehta001 Jul 21, 2026
cb9e5be
Describe Olive traceback redaction accurately
bmehta001 Jul 21, 2026
8c16bb0
Preserve falsy Olive telemetry map keys
bmehta001 Jul 21, 2026
81b2c90
Scrub Olive metadata keys recursively
bmehta001 Jul 21, 2026
2c81424
Redact unknown Olive config object names
bmehta001 Jul 21, 2026
f2e5c05
Remove the stale Olive device ID comment
bmehta001 Jul 21, 2026
767129c
Close partially initialized Olive telemetry stores
bmehta001 Jul 21, 2026
8fd10ad
Retry failed Olive telemetry initialization
bmehta001 Jul 21, 2026
76f27d3
Keep live Olive drain locks with the uploader
bmehta001 Jul 21, 2026
126e9be
Keep telemetry reusable after shutdown
bmehta001 Jul 21, 2026
0e750f1
Remove stale telemetry app-name constant
bmehta001 Jul 21, 2026
fb4ff4b
Wake only the active telemetry drainer
bmehta001 Jul 22, 2026
f1a87e3
Make Olive telemetry opt-out complete
bmehta001 Jul 23, 2026
d471235
Raise Olive telemetry error messages to 40 KB
bmehta001 Jul 24, 2026
fb60a93
Keep Olive telemetry helpers best-effort
bmehta001 Jul 24, 2026
c481a76
Apply Olive test opt-out before initialization
bmehta001 Jul 24, 2026
cfc3162
Align Olive telemetry schema with ONNX Runtime
bmehta001 Jul 25, 2026
2413f16
Align Olive telemetry with native context
bmehta001 Jul 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,8 @@ The sample chat app to run is found as [model-chat.py](https://github.com/micros
- [Recipes](https://github.com/microsoft/olive-recipes)

## Data/Telemetry
Distributions of this project may collect usage data and send it to Microsoft to help improve our products and services. See the [privacy statement](docs/Privacy.md) for more details.

This project may collect usage data and send it to Microsoft to help improve our products and services. See the [privacy statement](docs/Privacy.md) for details.

## 🤝 Contributions and Feedback
- We welcome contributions! Please read the [contribution guidelines](./CONTRIBUTING.md) for more details on how to contribute to the Olive project.
Expand All @@ -133,4 +134,3 @@ Licensed under the [MIT](./LICENSE) License.
[![Build Status](https://dev.azure.com/aiinfra/PublicPackages/_apis/build/status%2FOlive%20CI?label=Olive-CI)](https://dev.azure.com/aiinfra/PublicPackages/_build/latest?definitionId=1240)
[![Build Status](https://dev.azure.com/aiinfra/PublicPackages/_apis/build/status%2FOlive-ORT-Nightly?label=Olive-ORT-Nightly)](https://dev.azure.com/aiinfra/PublicPackages/_build/latest?definitionId=1279)


8 changes: 6 additions & 2 deletions docs/Privacy.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@ The software may collect information about you and your use of the software and
***

## Technical Details
Olive uses the [OpenTelemetry](https://opentelemetry.io/) API for its implementation. Telemetry is turned ON by default. Based on user consent, this data may be periodically sent to Microsoft servers following GDPR and privacy regulations for anonymity and data access controls. Application, device, and version information is collected automatically.
Telemetry is turned ON by default. Based on user consent, this data may be periodically sent to Microsoft servers following GDPR and privacy regulations for anonymity and data access controls. Application, device, and version information is collected automatically.

In addition, Olive may collect additional telemetry data such as:
- Invoked commands
- Performance data
- Exception information

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. Telemetry is also automatically disabled when a CI/CD environment is detected (e.g., GitHub Actions, Azure Pipelines, Jenkins). 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.
You can disable all telemetry by adding the `--disable_telemetry` flag to any Olive CLI command, or by setting the `ORT_DISABLE_TELEMETRY` environment variable to `1` before running. This full opt-out applies for the lifetime of the process.

In CI/CD environments (e.g., GitHub Actions, Azure Pipelines, Jenkins), Olive suppresses the device-id heartbeat and the 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 (including the default `LocalSystem` host) 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. Setting `ORT_DISABLE_TELEMETRY=1` in a CI/CD environment sends nothing at all.

Telemetry is implemented using only the Python standard library. Events are written to a local per-user SQLite queue and uploaded in the background to Microsoft over HTTPS. If telemetry is enabled but cannot be sent (for example, while offline), events remain in the local queue and are uploaded on a later run when a connection is available.
21 changes: 19 additions & 2 deletions olive/cli/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,9 @@ def _run_workflow(self):
mark_test_output_path(self.args.output_path)
print("Dry run mode enabled. Configuration file is generated but no optimization is performed.")
return None
workflow_output = olive_run(run_config)
workflow_output = olive_run(
run_config, recipe_telemetry_metadata=self._get_recipe_telemetry_metadata(), emit_error_telemetry=False
)
if is_test:
mark_test_output_path(self.args.output_path)
save_discrepancy_check_results(workflow_output, self.args.output_path)
Expand All @@ -349,6 +351,17 @@ def _run_workflow(self):
print(f"Model is saved at {self.args.output_path}")
return workflow_output

def _get_recipe_telemetry_metadata(self) -> dict[str, str]:
recipe_name = self.__class__.__name__
if recipe_name.endswith("Command"):
recipe_name = recipe_name[: -len("Command")]
return {
"recipe_name": recipe_name,
"recipe_command": recipe_name,
"recipe_source": "generated_cli",
"recipe_format": "generated",
}

@staticmethod
def _parse_extra_options(kv_items):
from onnxruntime_genai import __version__ as OrtGenaiVersion
Expand Down Expand Up @@ -995,7 +1008,11 @@ def add_search_options(sub_parser: ArgumentParser):

def add_telemetry_options(sub_parser: ArgumentParser):
"""Add telemetry options to the sub_parser."""
sub_parser.add_argument("--disable_telemetry", action="store_true", help="Disable telemetry for this command.")
sub_parser.add_argument(
"--disable_telemetry",
action="store_true",
help="Disable all telemetry for this process.",
)
return sub_parser


Expand Down
28 changes: 20 additions & 8 deletions olive/cli/launcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
# --------------------------------------------------------------------------
import os
import sys
from argparse import ArgumentParser
from warnings import warn
Expand Down Expand Up @@ -66,18 +67,29 @@ def main(raw_args=None, called_as_console_script: bool = True):

args, unknown_args = parser.parse_known_args(raw_args)

telemetry = Telemetry()
if args.disable_telemetry:
telemetry.disable_telemetry()

if not hasattr(args, "func"):
parser.print_help()
sys.exit(1)

# Run the command
service = args.func(parser, args, unknown_args)
service.run()
telemetry.shutdown()
# Honor --disable_telemetry before constructing Telemetry so the process creates
# no telemetry resources and never drains queued events.
disable_telemetry = getattr(args, "disable_telemetry", False)
previous_opt_out = os.environ.get("ORT_DISABLE_TELEMETRY")
if disable_telemetry:
os.environ["ORT_DISABLE_TELEMETRY"] = "1"
telemetry = None
try:
telemetry = Telemetry()
service = args.func(parser, args, unknown_args)
service.run()
finally:
if telemetry is not None:
telemetry.shutdown()
if disable_telemetry:
Comment thread
bmehta001 marked this conversation as resolved.
if previous_opt_out is None:
os.environ.pop("ORT_DISABLE_TELEMETRY", None)
else:
os.environ["ORT_DISABLE_TELEMETRY"] = previous_opt_out


def legacy_call(deprecated_module: str, command_name: str, *args):
Expand Down
27 changes: 24 additions & 3 deletions olive/cli/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,16 +55,22 @@ def register_subcommand(parser: ArgumentParser):

@action
def run(self):
from copy import deepcopy
from pathlib import Path

from olive.common.config_utils import load_config_file
from olive.workflows import run as olive_run

# allow the run_config to be a dict already (for api use)
run_config = self.args.run_config
if not isinstance(run_config, dict):
run_config = load_config_file(run_config)
run_config_input = self.args.run_config
run_config = (
deepcopy(run_config_input) if isinstance(run_config_input, dict) else load_config_file(run_config_input)
)
config_overrides = {}
if input_model_config := get_input_model_config(self.args, required=False):
print("Replacing input model config in run config")
run_config["input_model"] = input_model_config
config_overrides["input_model"] = input_model_config
elif self.args.test not in (None, False):
input_model = run_config.get("input_model")
if not isinstance(input_model, dict) or input_model.get("type", "").lower() != "hfmodel":
Expand All @@ -84,13 +90,28 @@ def run(self):
run_config.get("engine", {}).pop(rc_key, None)
# add value to run config directly
run_config[rc_key] = arg_value
config_overrides[rc_key] = arg_value

recipe_telemetry_metadata = {
"recipe_command": "WorkflowRun",
"recipe_source": "config_dict" if isinstance(run_config_input, dict) else "config_file",
"recipe_format": "dict"
if isinstance(run_config_input, dict)
else Path(run_config_input).suffix.lstrip(".").lower() or "unknown",
"execution_mode": "list_required_packages" if self.args.list_required_packages else "run",
"package_config_provided": bool(self.args.package_config),
}
if config_overrides:
recipe_telemetry_metadata["config_overrides"] = config_overrides

output_path = run_config.get("output_dir") or run_config.get("engine", {}).get("output_dir")
workflow_output = olive_run(
run_config,
list_required_packages=self.args.list_required_packages,
tempdir=self.args.tempdir,
package_config=self.args.package_config,
recipe_telemetry_metadata=recipe_telemetry_metadata,
emit_error_telemetry=False,
)
if self.args.test not in (None, False):
mark_test_output_path(output_path)
Expand Down
4 changes: 4 additions & 0 deletions olive/systems/docker/docker_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,8 @@ def _prepare_run_params(self) -> dict:

def _prepare_environment(self, base_env) -> dict:
"""Prepare environment variables for container."""
from olive.telemetry.telemetry import is_ci_environment

# Convert list to dict if needed
if isinstance(base_env, list):
environment = {env.split("=")[0]: env.split("=")[1] for env in base_env}
Expand All @@ -241,6 +243,8 @@ def _prepare_environment(self, base_env) -> dict:
# Add default environment variables
environment.setdefault("PYTHONPYCACHEPREFIX", "/tmp")
environment["OLIVE_LOG_LEVEL"] = logging.getLevelName(logger.getEffectiveLevel())
if is_ci_environment():
environment["CI"] = "1"

# Add HuggingFace token if needed
if self.hf_token:
Expand Down
12 changes: 11 additions & 1 deletion olive/systems/docker/workflow_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from olive.common.hf.login import huggingface_login
from olive.logging import get_olive_logger, set_verbosity_from_env
from olive.telemetry.telemetry import Telemetry
from olive.workflows import run as olive_run

logger = get_olive_logger()
Expand All @@ -20,7 +21,16 @@ def runner_entry(config):
config = json.load(f)

logger.info("Running workflow with config: %s", config)
olive_run(config)
try:
olive_run(config, emit_error_telemetry=False, emit_recipe_telemetry=False)
finally:
Comment thread
bmehta001 marked this conversation as resolved.
telemetry = Telemetry.get_existing_instance()
if telemetry is not None:
telemetry.shutdown(
timeout_millis=15_000,
callback_timeout_millis=15_000,
flush_seconds=15,
)


if __name__ == "__main__":
Expand Down
8 changes: 0 additions & 8 deletions olive/telemetry/constants.py

This file was deleted.

4 changes: 2 additions & 2 deletions olive/telemetry/deviceid/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from olive.telemetry.deviceid.deviceid import get_encrypted_device_id_and_status
from olive.telemetry.deviceid.deviceid import get_hashed_device_id_and_status

__all__ = ["get_encrypted_device_id_and_status"]
__all__ = ["get_hashed_device_id_and_status"]
27 changes: 20 additions & 7 deletions olive/telemetry/deviceid/_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@
REGISTRY_KEY = "deviceid"


def _chmod_best_effort(path: Path, mode: int) -> None:
try:
path.chmod(mode)
except OSError:
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
# Permission tightening is best-effort on filesystems that do not support chmod.
pass


class Store:
def __init__(self) -> None:
self._file_path: Path = self._build_path
Expand All @@ -21,9 +29,8 @@ def retrieve_id(self) -> str:
:return: The device id.
:rtype: str
"""
# check if file doesnt exist and raise an Exception
if not self._file_path.is_file():
raise FileExistsError(f"File {self._file_path.stem} does not exist")
raise FileNotFoundError(f"File {self._file_path.stem} does not exist")

return self._file_path.read_text(encoding="utf-8").strip()

Expand All @@ -33,10 +40,16 @@ def store_id(self, device_id: str) -> None:
:param str device_id: The device id to store.
:type device_id: str
"""
# create the folder location if it does not exist
self._file_path.parent.mkdir(parents=True, exist_ok=True)

self._file_path.touch()
# create the folder location if it does not exist, owner-only (0700) so other users on the
# machine cannot traverse into it to reach the device id.
self._file_path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
_chmod_best_effort(self._file_path.parent, 0o700)

# Owner-only (0600): the device id must not be world-readable by other users on the machine.
# touch(mode=...) creates it already restricted; chmod also tightens a pre-existing file before
# writing, so the id is never left at the umask default (commonly world-readable 0644).
self._file_path.touch(mode=0o600)
_chmod_best_effort(self._file_path, 0o600)
self._file_path.write_text(device_id, encoding="utf-8")
Comment thread
bmehta001 marked this conversation as resolved.


Expand Down Expand Up @@ -65,6 +78,6 @@ def store_id(self, device_id: str) -> None:
winreg.HKEY_CURRENT_USER,
REGISTRY_PATH,
reserved=0,
access=winreg.KEY_ALL_ACCESS | winreg.KEY_WOW64_64KEY,
access=winreg.KEY_SET_VALUE | winreg.KEY_CREATE_SUB_KEY | winreg.KEY_WOW64_64KEY,
) as key_handle:
winreg.SetValueEx(key_handle, REGISTRY_KEY, 0, winreg.REG_SZ, device_id)
17 changes: 5 additions & 12 deletions olive/telemetry/deviceid/deviceid.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class DeviceIdStatus(Enum):


_device_id_state = {"device_id": None, "status": DeviceIdStatus.NEW}
_DEVICE_ID_HASH_SALT = "olive:"


def get_device_id() -> str:
Expand Down Expand Up @@ -86,16 +87,8 @@ def get_device_id() -> str:
return device_id


def get_encrypted_device_id_and_status() -> tuple[str, DeviceIdStatus]:
"""Generate a FIPS-compliant encrypted device ID using SHA256 and returns the deviceIdStatus.

This method uses SHA256 which is FIPS 140-2 approved for cryptographic operations.
The device ID is hashed to ensure deterministic but secure device identification.

Returns:
str: FIPS-compliant encrypted device ID (base64-encoded)

"""
def get_hashed_device_id_and_status() -> tuple[str, DeviceIdStatus]:
"""Get the product-salted hashed device ID and its status."""
device_id = _device_id_state["device_id"] if _device_id_state["device_id"] is not None else get_device_id()
encrypted_device_id = hashlib.sha256(device_id.encode("utf-8")).digest().hex().upper() if device_id else ""
return encrypted_device_id, _device_id_state["status"]
hashed = hashlib.sha256(f"{_DEVICE_ID_HASH_SALT}{device_id}".encode()).hexdigest() if device_id else ""
return f"c:{hashed}" if hashed else "", _device_id_state["status"]
Loading
Loading